|
55266
|
1915
|
14
|
2026-05-18T13:59:24.261615+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112764261_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"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}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
PhostormVIewINavicarecode Project: faVsco.js, menu
PhostormVIewINavicarecodeLaravelRefactorFV faVsco.js°9 master k >ProiectC ActivityController.ongYC) ParentNode.ohrC) CoreUserRequest.pnp© RolesAndCrmOptionMismatchRule.phpc)SmsMessage.one© TransferUserAdminRule.phpv m Servicesv @ Activity>@ Aircall>@ AmazonConnect_ Apollo> @J AvayaC BaseService>_ bloobirasu Close07 CloudCallCloudtalkConnectAndSellD DemoDeskDialpad0 EightByEightFiveNineM GmaiD Gong• GoogleMeet• GoToMeetingM GrooveM LuhSootImportJiminnyaluctCallD MigratorNatterboxOffino0 Orumm OutreachC RingCentral© Client.phpC) Service.php© ThrottleHandler.php> C RingCentralVideoC7 SalesforceSales oftTalkdeskM TeamsTelusM Twilion TwilloflexM TwilioflexDirectM TwilioVidenM Uinloade"N VonaadM Xanti• M7oom58 €63095 C888889205C BaseService.php© SoftPhoneManager.phpscimProvistoning.ong© CoreUser.php© Activity/RingCentral/Service.php Xclass service extends Activityproviderservice impLements Importsbataintertace m A21 M 4 .11 ^ Vpublic function getInstallationStrategyO: string{...}public function onActivityProviderIntegrated(): void(...}public function supportedactiv1tychannelso: arrav-...* @throws SocialAccountTokeninvauidExcent.ion* dthrowsWiminnu Exceotions HttoBadRequestExcention* dthrowsWiminnu Excentions HttoForbiddenExcent.ion* dthrowsWiminnu Excentions HttoNotFoundExcent.ion* Othrows Jiminny Exceptions QuotaExceededException* Othrows Jiminny Exceptions ServiceUnavailableExceptionpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?stringif (Suser !== null) {$this->logger->warning("User filter is not available with this provider');Sthis->hasSalesforceInteqrated = Sthis->team->crm->fields( ->where([crm provlder10 → selt..cKM CALL_1U.'obnect type => "task])->existsoSthis->cachedCrmServiceDecorator = resolve(namCachedcrmservicebecorator::class)sth1s->cachedcrmserv1cebecorator->setcrmserv1cesth1s->crmserv1cerif (Sactivitvid) &$call = Sthis->client-›qetCallLoa(f'view' => 'Detailed'1. SactivitvId):if (Scall =e= nulb <return 0:sthis->loagen->dehua/*fRinaCentnal)AniResnonse' Scalla-neturn(int) Sthis-simnontcalldCall.•createEromPauload/Scall))e* every XX:15, XX:30, and XX:45 we look back 1 hour,* while at every round hour XX:00 we look back 6 hours= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch Filterder1n1t1on:› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >135136 @t›146 C>151 G>1usadeorivate const int No GROUP 10 = 999orivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray@: array{...hprivate function getOptions(): array{...}public function getValue: array{...}lprivate function getDefaultValue@: array{...}public function getValidationRules(2string Sprefix = null): arrav{...}lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}[Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:23CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 05-127 UTF.8io 4 spaces...
|
55264
|
NULL
|
NULL
|
NULL
|
|
55267
|
1914
|
28
|
2026-05-18T13:59:29.864370+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112769864_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI...
|
[{"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
-3088599111841370244
|
-8636775528843997756
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left100% C8 • Mon 18 May 16:59:295Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:45:56...
|
55265
|
NULL
|
NULL
|
NULL
|
|
55268
|
1915
|
15
|
2026-05-18T13:59:30.382794+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112770382_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
[{"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":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"21","depth":4,"bounds":{"left":0.38131648,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.3929521,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.40292552,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.17318435,"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.4192154,"top":0.17318435,"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\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-4305373452071923642
|
-1646255984498593726
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55269
|
1914
|
29
|
2026-05-18T13:59:32.864715+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112772864_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
-4235983745889776938
|
-8204421443435123770
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left100% C8 • Mon 18 May 16:59:3220 5••Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:45:59...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55270
|
1914
|
30
|
2026-05-18T13:59:33.623146+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112773623_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left100% C8 • Mon 18 May 16:59:33)40 5Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:46:00...
|
NULL
|
-6434945452674660002
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left100% C8 • Mon 18 May 16:59:33)40 5Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:46:00...
|
55269
|
NULL
|
NULL
|
NULL
|
|
55271
|
1915
|
16
|
2026-05-18T13:59:33.647653+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112773647_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormVIewINavicarecodeLaravelRefactorFV faVsco. PhpStormVIewINavicarecodeLaravelRefactorFV faVsco.js°9 master k >ProiectC ActivityController.ongYC) ParentNode.ohr(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.phpc)SmsMessage.one© TransferUserAdminRule.phpv m Servicesv @ Activity>@ Aircall>@ AmazonConnect_Apollo> @J AvayaC BaseService>_ bloobirasuClose07 CloudCallCloudtalkConnectAndSellD DemoDeskDialpad0 EightByEightFiveNineM GmaiD Gong• GoogleMeet• GoToMeetingM GrooveM LuhSootImportJiminnyaluctCallD MigratorNatterboxOffino0 Orumm OutreachC RingCentral© Client.phpC) Service.php© ThrottleHandler.php> C RingCentralVideoC7 SalesforceSales oftTalkdeskM TeamsTelusM Twilion TwilloflexM TwilioflexDirectM TwilioVidenM Uinloade"N VonaadM Xanti• M7oom58 €630790110|121C BaseService.php© SoftPhoneManager.phpscimProvistoning.ong© CoreUser.php© Activity/RingCentral/Service.php Xclass service extends Activityproviderservice impLements Importsbataintertace m A21 M 4 .11 ^ Vpublic function getInstallationStrategyO: string{...}public function onActivityProviderIntegrated(): void(...}public function supportedactiv1tychannelso: arrav-...* @throws SocialAccountTokeninvauidExcent.ion* dthrowsWiminnu Exceotions HttoBadRequestExcention* dthrowsWiminnu Excentions HttoForbiddenExcent.ion* dthrowsWiminnu Excentions HttoNotFoundExcent.ion* Othrows Jiminny Exceptions QuotaExceededException* Othrows Jiminny Exceptions ServiceUnavailableExceptionpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User Suser = null, ?stringif (Suser !== null) {$this->logger->warning("User filter is not available with this provider');Sthis->hasSalesforceInteqrated = Sthis->team->crm->fields( ->where([crm provlder10 → selt..cKM CALL_1U.'obnect type' = "task',])->existsosth1s->cachedcrmservicebecorator = resolve@name: Cachedcrmservicebecorator::class):sth1s->cachedcrmserv1cebecorator->setcrmserv1cesth1s->crmserv1ceif (Sactivitvid) &$call = Sthis->client-›qetCallLoa(f'view' => 'Detailed']. Sactivitvld).if (Scall =e= nulb <return 0:$this->logger->debug('[RingCentral) ApiResponse', $call):neturn(int)_Sthis->imnon+callCall.•croateFromPauload/Scall))* every XX:15, XX:30, and XX:45 we look back 1 hour,* while at every round hour XX:00 we look back 6 hours= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch Filterder1n1t1on:› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >118135136 @t›146 C>151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}lprivate function getDefaultValue@: array{...}public function getValidationRules(2string Sprefix = null): arrav{...}lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}[Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:33CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6W Windsurf Toams 107-17 UTF.8io 4 spaces...
|
NULL
|
-7360107536640100479
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormVIewINavicarecodeLaravelRefactorFV faVsco. PhpStormVIewINavicarecodeLaravelRefactorFV faVsco.js°9 master k >ProiectC ActivityController.ongYC) ParentNode.ohr(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.phpc)SmsMessage.one© TransferUserAdminRule.phpv m Servicesv @ Activity>@ Aircall>@ AmazonConnect_Apollo> @J AvayaC BaseService>_ bloobirasuClose07 CloudCallCloudtalkConnectAndSellD DemoDeskDialpad0 EightByEightFiveNineM GmaiD Gong• GoogleMeet• GoToMeetingM GrooveM LuhSootImportJiminnyaluctCallD MigratorNatterboxOffino0 Orumm OutreachC RingCentral© Client.phpC) Service.php© ThrottleHandler.php> C RingCentralVideoC7 SalesforceSales oftTalkdeskM TeamsTelusM Twilion TwilloflexM TwilioflexDirectM TwilioVidenM Uinloade"N VonaadM Xanti• M7oom58 €630790110|121C BaseService.php© SoftPhoneManager.phpscimProvistoning.ong© CoreUser.php© Activity/RingCentral/Service.php Xclass service extends Activityproviderservice impLements Importsbataintertace m A21 M 4 .11 ^ Vpublic function getInstallationStrategyO: string{...}public function onActivityProviderIntegrated(): void(...}public function supportedactiv1tychannelso: arrav-...* @throws SocialAccountTokeninvauidExcent.ion* dthrowsWiminnu Exceotions HttoBadRequestExcention* dthrowsWiminnu Excentions HttoForbiddenExcent.ion* dthrowsWiminnu Excentions HttoNotFoundExcent.ion* Othrows Jiminny Exceptions QuotaExceededException* Othrows Jiminny Exceptions ServiceUnavailableExceptionpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User Suser = null, ?stringif (Suser !== null) {$this->logger->warning("User filter is not available with this provider');Sthis->hasSalesforceInteqrated = Sthis->team->crm->fields( ->where([crm provlder10 → selt..cKM CALL_1U.'obnect type' = "task',])->existsosth1s->cachedcrmservicebecorator = resolve@name: Cachedcrmservicebecorator::class):sth1s->cachedcrmserv1cebecorator->setcrmserv1cesth1s->crmserv1ceif (Sactivitvid) &$call = Sthis->client-›qetCallLoa(f'view' => 'Detailed']. Sactivitvld).if (Scall =e= nulb <return 0:$this->logger->debug('[RingCentral) ApiResponse', $call):neturn(int)_Sthis->imnon+callCall.•croateFromPauload/Scall))* every XX:15, XX:30, and XX:45 we look back 1 hour,* while at every round hour XX:00 we look back 6 hours= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch Filterder1n1t1on:› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >118135136 @t›146 C>151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}lprivate function getDefaultValue@: array{...}public function getValidationRules(2string Sprefix = null): arrav{...}lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}[Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:33CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6W Windsurf Toams 107-17 UTF.8io 4 spaces...
|
55268
|
NULL
|
NULL
|
NULL
|
|
55272
|
1914
|
31
|
2026-05-18T13:59:35.867633+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112775867_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections...
|
[{"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
-7429716278976468786
|
-8636355650190325311
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left100% C8 • Mon 18 May 16:59:35)A05Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement ®1:46:02...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55273
|
1915
|
17
|
2026-05-18T13:59:37.544011+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112777544_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"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":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
-4235983745889776938
|
-8204421443435123770
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
PhostormVIewINavicarecodeLaravelKeractorFV faVsco.js°9 master kroledeyC ActivityController.ong© SoftPhoneManager.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euy© ParentNode.php© RolesAndCrmOptionMismatchRule.php©) Smsmessage.pnp© TransferUserAdminRule.phpv m Servicesv @ Activity>@ Aircall>@ AmazonConnect_Apollo> @J AvayaC BaseService>_ bloobirasuCloseCloudcallCloudtalkConnectAndSellD DemoDeskDialpadD EightByEightFiveNineM GmaillD Gong• GoogleMeet• GoToMeetingM GrooveM LuhSootImportJiminnyaluctCallD MigratorNatterbox0 Office0 OrumC Outreach• C RingCentral© Client.phpC) Service.php© ThrottleHandler.php> C RingCentralVideoC7 SalesforceSales oftTalkdeskM TeamsTelusn Twilion TwilloflexM TwiloflexDirectM TwilioVidenM Uinloade"N VonaadM Xanti> M7oom(C) CoreUserRequest.onpscimProvistoning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php X<?php© Activity/Close/service.php© Activity/RingCentral/Service.php xdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*class Service extends Activityproviderservice impLements Importsbataintertace m| A21 ×411 ^ v42 Ci 047 053 0101)58 0163 01170 0195 €102.104private const CALL CACHE CONCURRENCY TIMEOUT = 10; // 10 seconds› use ...s usagesprivate const CALL CACHE COMPLETED = 'completed' :final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterfaceprivate const CALL CACHE COMPLETED TIMEOUT = 3600: // 1 hour1usadeorivate const int No GROUP 10 = 999procected scring sname = ACCIVICY::PRUVIUEK KLNGCENIRAL"private bool ShasSalesforceIntegrated = false:NANorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hprivate CachedCrmServiceDecorator $cachedCrmServicebecorator;29 Ф >public function shouldApplyQueries: boolf...}public function __construct(?array $config = []){...}34 6>public function getQueries: FilterDefinitionQueryCollection...hpublic function getDisplayName: stringt...45 đ >public function toArray@: array{...hpublic function getinstallationStrategy: stringt...}private function getOptions(): array{...}public function onActivityProviderIntegrated: void...}— 118=119public function getValue®: array{...}public function supportedActivityChannels: arrayt...* Athrows SocialAccountTokenTnvalidFycention=131 >135=136 Gt >private function getDefaultValue: array{...}* @throws Jiminny\Exceptions\HttpBadRequestException* @throwsJiminny\Exceptions\HttpForbiddenException* @throws Jiminny\Exceptions\HttpNotFoundExceptionpublic function aetValidationRules(2strina Sorefix = null): arrav{...?l* acnnows. Jiminny.cxcepczons uuocacxceededexcepc1on* acnrows Jiminnu excepcions serviceunavazladlecxcepcion146 Gt ›150— 151 G;public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string 156if (Suser !== null) {Sthis->loqger->warning('User filter is not available with this provider'):Sthis->hasSalesforceIntegrated = Sthis->team->crm->fields()->where(['erm provider id' = self:-CRM CALL 1D.1->existso:sthic->cachedcrmServicpleconaton =npcolvedname:CachedtnmServzcpheconaton..clacs)cthie-scachedcrmServiceleconaton-scottrmServicecthic->crmServicp[Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:37CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Teams107-12UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55274
|
1914
|
32
|
2026-05-18T13:59:37.557152+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112777557_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
-5794999150126922245
|
-8204417045355058234
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left)100% C8 • Mon 18 May 16:59:37A05Galya Dimitrova,Nikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:46:04...
|
55272
|
NULL
|
NULL
|
NULL
|
|
55275
|
1914
|
33
|
2026-05-18T13:59:40.959772+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112780959_m1.jpg...
|
PhpStorm
|
Find Usages
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find Usages
text/html
text/html
text/html
Method & Find Usages
text/html
text/html
text/html
Method 'importData()' of class 'Service' implements method in 'ImportsDataInterface'<br/>Do you want to find usages of base method?
Don't ask again
Cancel
Current Method
Base Method...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"Find Usages","depth":1,"on_screen":true,"value":"Find Usages","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Method 'importData()' of class 'Service' implements method in 'ImportsDataInterface'<br/>Do you want to find usages of base method?","depth":1,"on_screen":true,"help_text":"text/html","role_description":"text"},{"role":"AXCheckBox","text":"Don't ask again","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Current Method","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Base Method","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8826040943620210519
|
8402355453214726924
|
visual_change
|
accessibility
|
NULL
|
Find Usages
text/html
text/html
text/html
Method & Find Usages
text/html
text/html
text/html
Method 'importData()' of class 'Service' implements method in 'ImportsDataInterface'<br/>Do you want to find usages of base method?
Don't ask again
Cancel
Current Method
Base Method...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55276
|
1915
|
18
|
2026-05-18T13:59:42.493332+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112782493_m2.jpg...
|
PhpStorm
|
Find Usages
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find Usages
text/html
text/html
text/html
Method & Find Usages
text/html
text/html
text/html
Method 'importData()' of class 'Service' implements method in 'ImportsDataInterface'<br/>Do you want to find usages of base method?
Don't ask again
Cancel
Current Method
Base Method...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"Find Usages","depth":1,"bounds":{"left":0.44481382,"top":0.28092578,"width":0.124667555,"height":0.014365523},"on_screen":true,"value":"Find Usages","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"bounds":{"left":0.44481382,"top":0.28092578,"width":0.027593086,"height":0.014365523},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Method 'importData()' of class 'Service' implements method in 'ImportsDataInterface'<br/>Do you want to find usages of base method?","depth":1,"bounds":{"left":0.44481382,"top":0.30167598,"width":0.124667555,"height":0.040702313},"on_screen":true,"help_text":"text/html","role_description":"text"},{"role":"AXCheckBox","text":"Don't ask again","depth":1,"bounds":{"left":0.44348404,"top":0.3519553,"width":0.12599733,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.44481382,"top":0.38707104,"width":0.028590426,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Current Method","depth":1,"bounds":{"left":0.47739363,"top":0.38707104,"width":0.04720745,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Base Method","depth":1,"bounds":{"left":0.52859044,"top":0.38707104,"width":0.04089096,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8826040943620210519
|
8402355453214726924
|
click
|
accessibility
|
NULL
|
Find Usages
text/html
text/html
text/html
Method & Find Usages
text/html
text/html
text/html
Method 'importData()' of class 'Service' implements method in 'ImportsDataInterface'<br/>Do you want to find usages of base method?
Don't ask again
Cancel
Current Method
Base Method...
|
55273
|
NULL
|
NULL
|
NULL
|
|
55277
|
1914
|
34
|
2026-05-18T13:59:43.977113+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112783977_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Searching for importData in 110 files…
Project: fa Searching for importData in 110 files…
Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Searching for importData in 110 files…","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","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}]...
|
1448049054652375250
|
-8744856731639485502
|
visual_change
|
hybrid
|
NULL
|
Searching for importData in 110 files…
Project: fa Searching for importData in 110 files…
Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left)100% C8 • Mon 18 May 16:59:43)5Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:46:10...
|
55275
|
NULL
|
NULL
|
NULL
|
|
55278
|
1915
|
19
|
2026-05-18T13:59:46.354815+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112786354_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Searching for importData in 110 files…
Project: fa Searching for importData in 110 files…
Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Searching for importData in 110 files…","depth":2,"bounds":{"left":0.6449468,"top":0.92098963,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.6449468,"top":0.952913,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"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":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"21","depth":4,"bounds":{"left":0.38131648,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.3929521,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.40292552,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.17318435,"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.4192154,"top":0.17318435,"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\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
1516081236813873545
|
-1646255984498593726
|
visual_change
|
accessibility
|
NULL
|
Searching for importData in 110 files…
Project: fa Searching for importData in 110 files…
Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55279
|
1914
|
35
|
2026-05-18T13:59:47.033777+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112787033_m1.jpg...
|
iTerm2
|
NULL
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform) Refinemen... 1 m left)100% C8 • Mon 18 May 16:59:46)=20 5Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement ®1:46:13...
|
NULL
|
-5312696085512556362
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform) Refinemen... 1 m left)100% C8 • Mon 18 May 16:59:46)=20 5Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement ®1:46:13...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55280
|
1915
|
20
|
2026-05-18T13:59:49.442223+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112789442_m2.jpg...
|
iTerm2
|
NULL
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormViewINavicarecodeLaravelKeractorFV faVsco. PhpStormViewINavicarecodeLaravelKeractorFV faVsco.js°9 master k >ProiectC ActivityController.ong© ParentNode.php© RolesAndCrmOptionMismatchRule.php(C) CoreUserRequest.onp(c)SmsMessage.pnp© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnectJ Apollo> @J AvayaC BaseService>@ BloobirdsuCloseu ClouccallCloudtalkConnectAndSellDemoDeskDialpadh Eichtevziahi(Jiminny|Services\Activity\RingCentral\Service.importData in ... xMethodlm) d imnortbata Service lann/Services/Activitv/RinaCentral• Ucades in Droiect Silec 1reculWindowC BaseService.pnp© SoftPhoneManager.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyc ScimProvisioning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xch RoleAttrTest.ongfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php xccrm/close/service.pho29 Oclass service extends Activityproviderservice impLements Importsbataintertace mA21 ×411 ^ v 33340>* acnrows Jiminny.cxcepczons Hcconocroundexcept10n* @throwsJiminny\Exceptions\QuotaExceededException* dcnrows Jiminnu excepcions serviceunavazladlecxcepclon= 44T 624So)95 Gtublic function importbatalzcarbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringl118if (Suser !== null) {sthis->loqger-warning('User filter is not available with this provider')E 130=131 ›sthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oi= self::CRM_CALL_ID,136 0›public function shouldApplyQueries: boolk...,public function getQueries): FilterDefinitionQueryCollectiont...public function toArray@: arrayt...private function getOptions): arrayi...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function qetValidationRules(?string Sprefix = null): arrayi...}CascadeImplement Trial Owne: [Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:48U AskJiminnyReportActivityServiceTest vFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineView allWARN Metadata found iin doc-comment for methodent/SCIM/ Constants.php +3ann/Comnonent/SCIM/ M ScimProvisionina.0hn +85-15app/DTO/SCIM/AAD/Response/ @ CoreUser.php +21app/Component/SCIM/Mutators/Attributes/User/# RoleAttr.oho +171onent/SCIM/Mutators/Attributes/User/RoleAttriest.ono +224Ask anything (2AL)+ <>Code SWE-1.6• Reiect all IAccept alliNothina to showCall HierarchyW Windsurf Toams 05-21/10 charc) UTF.8 #l A enadoe...
|
NULL
|
4054164560984030124
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormViewINavicarecodeLaravelKeractorFV faVsco. PhpStormViewINavicarecodeLaravelKeractorFV faVsco.js°9 master k >ProiectC ActivityController.ong© ParentNode.php© RolesAndCrmOptionMismatchRule.php(C) CoreUserRequest.onp(c)SmsMessage.pnp© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnectJ Apollo> @J AvayaC BaseService>@ BloobirdsuCloseu ClouccallCloudtalkConnectAndSellDemoDeskDialpadh Eichtevziahi(Jiminny|Services\Activity\RingCentral\Service.importData in ... xMethodlm) d imnortbata Service lann/Services/Activitv/RinaCentral• Ucades in Droiect Silec 1reculWindowC BaseService.pnp© SoftPhoneManager.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyc ScimProvisioning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xch RoleAttrTest.ongfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php xccrm/close/service.pho29 Oclass service extends Activityproviderservice impLements Importsbataintertace mA21 ×411 ^ v 33340>* acnrows Jiminny.cxcepczons Hcconocroundexcept10n* @throwsJiminny\Exceptions\QuotaExceededException* dcnrows Jiminnu excepcions serviceunavazladlecxcepclon= 44T 624So)95 Gtublic function importbatalzcarbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringl118if (Suser !== null) {sthis->loqger-warning('User filter is not available with this provider')E 130=131 ›sthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oi= self::CRM_CALL_ID,136 0›public function shouldApplyQueries: boolk...,public function getQueries): FilterDefinitionQueryCollectiont...public function toArray@: arrayt...private function getOptions): arrayi...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function qetValidationRules(?string Sprefix = null): arrayi...}CascadeImplement Trial Owne: [Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:48U AskJiminnyReportActivityServiceTest vFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineView allWARN Metadata found iin doc-comment for methodent/SCIM/ Constants.php +3ann/Comnonent/SCIM/ M ScimProvisionina.0hn +85-15app/DTO/SCIM/AAD/Response/ @ CoreUser.php +21app/Component/SCIM/Mutators/Attributes/User/# RoleAttr.oho +171onent/SCIM/Mutators/Attributes/User/RoleAttriest.ono +224Ask anything (2AL)+ <>Code SWE-1.6• Reiect all IAccept alliNothina to showCall HierarchyW Windsurf Toams 05-21/10 charc) UTF.8 #l A enadoe...
|
55278
|
NULL
|
NULL
|
NULL
|
|
55281
|
1914
|
36
|
2026-05-18T13:59:50.027724+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112790027_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left)100% C8 • Mon 18 May 16:59:49A05Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement ®1:46:16...
|
NULL
|
5481202023529347957
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left)100% C8 • Mon 18 May 16:59:49A05Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement ®1:46:16...
|
55279
|
NULL
|
NULL
|
NULL
|
|
55283
|
1915
|
21
|
2026-05-18T13:59:52.969333+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112792969_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 master kProiect© ParentNode.php(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.php(c)SmsMessage.pnp© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnectJ Apollo> @J AvayaC BaseService95 Gt› _ bloobirasuCloseCloudcallCloudtalkConnectAndSellD DemoDeskDialpadl sichisvziahFiveNineN GmaillWiminny Services Activitv RinaCentr• Methodm d importbata service ...app/services/Activity RinaCen> Usages in Prolect Files 1 result* [Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:52C BaseService.pnp= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyc ScimProvisioning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php x29 Oclass service extends Activityproviderservice impLements Importsbatalntertace mA21 M4 M1AV340>* acnrows. Jiminny.cxcepczons Heconocroundexcept10r* @throwsJiminny\Exceptions\QuotaExceededException= 444So)* dcnrows Jiminnu excepcions serviceunavazladlecxcepclonublic function importbata@carbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringif (suser == nulusthis->loqger-warnina'User filter 1s not avallable with this provider'oisthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oierm orovider id'= self::CRM CALL IDI=155136 @›'object_type' => 'task',1->existso:public function shouldApplyQueries: booll...,public function getQueries: FilterDefinitionQueryCollectiont...rpublic function toArray@: arrayt...private function getOptions): array{...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function getValidationRules(?string Sprefix = null): array{...}CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role ManagemeWARN Metadata found iin doc-comment for method9/ 10 tacke doneo files/U ListenerkoiecannotHaveAaminurmanagerrermissionkuie.pnp +39app/Comoonent/SCIM/=Constants.ono +3se/ b coreUser.phpann/Comnonent/SCIM/Mutatore/Attributes/Ucer/M PoleAtr.nbo 4171tes/User/ D RoleAttrTest.php +224ann/DTO/SCIMIAAD/Request/MCoreUserRequest.ohn t15Ask anvthina (*4D1+ <> Code SWE-1.6+0 ..inp on LineView all* Reject allAccent alll8U40»×95 €* @throws SocialAccountTokenInvalidSxcention* @throws Jiminny Exceptions HttpBadRequestException*AthrowsWaiminnuFxcentionc HttnForhiddenFycentzon* @throws Jiminny\Exceptions\HttpNotFoundException* @thnows Jiminny\Exceptions\QuotaExceededException* @thnows Jiminny\Exceptions|ServiceUnavailableExceptionpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?intif (Suser !== null) {Sthis->logger->warning('User filter is not available with this provider'):Sthis->hasSalesforceIntegrated = Sthis->team->crm->fields( ->where(П'crm_provider id' => self::CRM_CALL_ID'object_type' => "task',1)->existsO:sthas->cachedermse/cebecorator = resolve(CachedCrmServicebecorator::class):Sthis->cachedCrmServiceDecorator->setCrmService(sthis->crmService)sif (Sactivitvld){$call = Sthis->cLient->getCallLoa(['view' => 'Detailed'1. Sactivitvid):if (Scall ee= null)dCall HierarchyW Windsurf Toams 05.21/10 charc)UTF.8# henssoe...
|
NULL
|
-3734705326483816792
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 master kProiect© ParentNode.php(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.php(c)SmsMessage.pnp© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnectJ Apollo> @J AvayaC BaseService95 Gt› _ bloobirasuCloseCloudcallCloudtalkConnectAndSellD DemoDeskDialpadl sichisvziahFiveNineN GmaillWiminny Services Activitv RinaCentr• Methodm d importbata service ...app/services/Activity RinaCen> Usages in Prolect Files 1 result* [Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:52C BaseService.pnp= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyc ScimProvisioning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php x29 Oclass service extends Activityproviderservice impLements Importsbatalntertace mA21 M4 M1AV340>* acnrows. Jiminny.cxcepczons Heconocroundexcept10r* @throwsJiminny\Exceptions\QuotaExceededException= 444So)* dcnrows Jiminnu excepcions serviceunavazladlecxcepclonublic function importbata@carbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringif (suser == nulusthis->loqger-warnina'User filter 1s not avallable with this provider'oisthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oierm orovider id'= self::CRM CALL IDI=155136 @›'object_type' => 'task',1->existso:public function shouldApplyQueries: booll...,public function getQueries: FilterDefinitionQueryCollectiont...rpublic function toArray@: arrayt...private function getOptions): array{...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function getValidationRules(?string Sprefix = null): array{...}CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role ManagemeWARN Metadata found iin doc-comment for method9/ 10 tacke doneo files/U ListenerkoiecannotHaveAaminurmanagerrermissionkuie.pnp +39app/Comoonent/SCIM/=Constants.ono +3se/ b coreUser.phpann/Comnonent/SCIM/Mutatore/Attributes/Ucer/M PoleAtr.nbo 4171tes/User/ D RoleAttrTest.php +224ann/DTO/SCIMIAAD/Request/MCoreUserRequest.ohn t15Ask anvthina (*4D1+ <> Code SWE-1.6+0 ..inp on LineView all* Reject allAccent alll8U40»×95 €* @throws SocialAccountTokenInvalidSxcention* @throws Jiminny Exceptions HttpBadRequestException*AthrowsWaiminnuFxcentionc HttnForhiddenFycentzon* @throws Jiminny\Exceptions\HttpNotFoundException* @thnows Jiminny\Exceptions\QuotaExceededException* @thnows Jiminny\Exceptions|ServiceUnavailableExceptionpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?intif (Suser !== null) {Sthis->logger->warning('User filter is not available with this provider'):Sthis->hasSalesforceIntegrated = Sthis->team->crm->fields( ->where(П'crm_provider id' => self::CRM_CALL_ID'object_type' => "task',1)->existsO:sthas->cachedermse/cebecorator = resolve(CachedCrmServicebecorator::class):Sthis->cachedCrmServiceDecorator->setCrmService(sthis->crmService)sif (Sactivitvld){$call = Sthis->cLient->getCallLoa(['view' => 'Detailed'1. Sactivitvid):if (Scall ee= null)dCall HierarchyW Windsurf Toams 05.21/10 charc)UTF.8# henssoe...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55282
|
1914
|
37
|
2026-05-18T13:59:53.260499+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112793260_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Searching for importData in 110 files…
Project: fa Searching for importData in 110 files…
Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Searching for importData in 110 files…","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","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\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1516081236813873545
|
-1646255984498593726
|
visual_change
|
accessibility
|
NULL
|
Searching for importData in 110 files…
Project: fa Searching for importData in 110 files…
Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55284
|
1914
|
38
|
2026-05-18T13:59:56.283155+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112796283_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","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\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Activity\\RingCentral;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse JsonException;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Call;\nuse Jiminny\\DTO\\ImportCall\\RingCentral\\Extension;\nuse Jiminny\\Events\\Activities\\Dialers\\CallImported;\nuse Jiminny\\Events\\Activities\\Provider\\SyncActivityProviderEvent;\nuse Jiminny\\Exceptions\\RingCentralExtensionNotFound;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\RingCentral\\DownloadTrack;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Activity\\ActivityProviderService;\nuse Jiminny\\Services\\Activity\\RingCentral\\Client as RingCentralClient;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Sentry\\Laravel\\Facade as Sentry;\nuse Throwable;\n\nclass Service extends ActivityProviderService implements ImportsDataInterface\n{\n private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';\n private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';\n\n private const CALL_CACHE_CONCURRENCY = 'concurrency';\n private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds\n private const CALL_CACHE_COMPLETED = 'completed';\n private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour\n\n protected string $name = Activity::PROVIDER_RINGCENTRAL;\n private bool $hasSalesforceIntegrated = false;\n\n private CachedCrmServiceDecorator $cachedCrmServiceDecorator;\n\n public function __construct(?array $config = [])\n {\n parent::__construct();\n $this->client = new RingCentralClient($config);\n }\n\n public function getDisplayName(): string\n {\n return 'RingCentral';\n }\n\n public function getInstallationStrategy(): string\n {\n return self::INSTALLATION_STRATEGY_OAUTH;\n }\n\n public function onActivityProviderIntegrated(): void\n {\n if (! $this->hasProvider()) {\n $this->setupActivityProvider();\n }\n\n $this->setSocialAccount(null);\n $response = $this->client->get('/restapi/v1.0/account/~');\n $this->provider->update(['provider_id' => $response['id'] ?? null]);\n\n event(new SyncActivityProviderEvent($this->getProvider(), [\n 'start_date' => now()->subMonth(),\n 'end_date' => now(),\n ]));\n }\n\n public function supportedActivityChannels(): array\n {\n return [\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ];\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws \\Jiminny\\Exceptions\\HttpBadRequestException\n * @throws \\Jiminny\\Exceptions\\HttpForbiddenException\n * @throws \\Jiminny\\Exceptions\\HttpNotFoundException\n * @throws \\Jiminny\\Exceptions\\QuotaExceededException\n * @throws \\Jiminny\\Exceptions\\ServiceUnavailableException\n */\n public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int\n {\n if ($user !== null) {\n $this->logger->warning('User filter is not available with this provider');\n }\n\n $this->hasSalesforceIntegrated = $this->team->crm->fields()->where([\n 'crm_provider_id' => self::CRM_CALL_ID,\n 'object_type' => 'task',\n ])->exists();\n\n $this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);\n $this->cachedCrmServiceDecorator->setCrmService($this->crmService);\n\n if ($activityId) {\n $call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);\n\n if ($call === null) {\n return 0;\n }\n\n $this->logger->debug('[RingCentral] ApiResponse', $call);\n\n return (int) $this->importCall(Call::createFromPayload($call));\n }\n\n /**\n * every XX:15, XX:30, and XX:45 we look back 1 hour,\n * while at every round hour XX:00 we look back 6 hours\n */\n $lookupCandidatePeriod = now()->subHour();\n if ($from === null || $from->isAfter($lookupCandidatePeriod)) {\n $from = $lookupCandidatePeriod;\n }\n\n if ($to === null) {\n $to = now();\n }\n\n $syncCount = 0;\n $offset = 1;\n $payload = [\n 'perPage' => 1000,\n 'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),\n 'dateTo' => $to->format(DATE_RFC3339_EXTENDED),\n 'type' => ['Voice'],\n 'view' => 'Detailed',\n ];\n\n do {\n $payload['page'] = $offset;\n\n try {\n $response = $this->client->getCallLog($payload);\n\n if ($response === null) {\n return $syncCount;\n }\n\n $this->logger->info(\n '[RingCentral] Retrieved call log',\n [\n 'teamId' => $this->team->id_string,\n 'totalCalls' => count($response['records']),\n 'payload' => $payload,\n ]\n );\n\n foreach ($response['records'] as $call) {\n $ringCentralCall = Call::createFromPayload($call);\n\n try {\n $syncCount += (int) $this->importCall($ringCentralCall);\n } catch (RingCentralExtensionNotFound $exception) {\n $this->logger->warning('RingCentral extension not found', [\n 'team' => $this->team->id_string,\n 'extension' => $exception->getExtensionId(),\n ]);\n\n Sentry::captureException($exception);\n }\n }\n } catch (Throwable $exception) {\n $this->logger->error('[RingCentral] Import has failed', [\n 'team' => $this->team->getSlug(),\n 'socialAccountId' => $this->socialAccount->getId(),\n 'code' => $exception->getCode(),\n 'exception' => $exception->getMessage(),\n ]);\n\n return $syncCount;\n }\n\n $offset++;\n } while (isset($response['navigation']['nextPage']));\n\n $this->provider->update(['last_synced_at' => now()]);\n\n return $syncCount;\n }\n\n private function importCall(Call $call): bool\n {\n $callId = $call->getCallId();\n\n if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)\n || $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))\n && ! $this->debugMode\n ) {\n $this->logger->info('[RingCentral] Call already imported', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);\n\n $callData = $call->getPayload();\n $isOutbound = $call->isDirectionOutbound();\n $playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;\n $fields = $task = [];\n\n $this->logger->info('[RingCentral] Syncing call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n 'callData' => $callData,\n ]);\n\n if ($this->isInternalCall($call)) {\n $this->logger->info('[RingCentral] Skipping internal call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! $call->isRecordingAvailable()) {\n $this->logger->warning('[RingCentral] Call has no recording', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n if (! isset($callData['from'], $callData['to'])) {\n $this->logger->warning('[RingCentral] Call is missing from or to data', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $user = $this->resolveUser($call);\n\n if (! $user instanceof User) {\n $this->logger->info('[RingCentral] No user found. Skipping call', [\n 'callId' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $existingCall = $user->activities()\n ->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n $callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);\n\n if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {\n $this->logger->info('[RingCentral] Call already exists', [\n 'call_id' => $callId,\n 'teamId' => $this->team->getId(),\n ]);\n\n return false;\n }\n\n $phone = $isOutbound\n ? $call->getCalleePhoneNumber()\n : $call->getCallerPhoneNumber();\n\n $title = 'Call ' . ($isOutbound ? 'to ' : 'from ');\n $playbook = $this->getPlaybook($user);\n\n if ($this->hasSalesforceIntegrated) {\n try {\n $fields = $this->team->crm->fields()->where('is_indexable', 1)\n ->where('object_type', 'task')->get()->all();\n\n $crmFilters = [\n self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',\n ];\n\n $task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);\n } catch (Exception $ex) {\n Sentry::captureException($ex);\n }\n }\n\n if (! empty($task) && $this->hasSalesforceIntegrated) {\n [$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);\n\n if ($task['Type']) {\n $playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);\n }\n } elseif ($phone !== null) {\n // Note in some cases phone might be null.\n [$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(\n $phone,\n null,\n $user->getId()\n );\n }\n\n if ($lead === null && $contact === null && $account === null) {\n $this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [\n 'person' => $callData['to'],\n ]);\n\n $title .= $phone;\n } else {\n $title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));\n }\n\n $startedAt = $call->getActualStartTime();\n $completedAt = $call->getActualEndTime();\n $duration = $call->getDuration();\n\n /** @var Activity|null $activity */\n $activity = $user->activities()->withTrashed()\n ->where('telephony_provider_id', $callId)\n ->where('provider', $this->getName())\n ->first();\n\n if ($activity?->trashed()) {\n $this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);\n\n return false;\n }\n\n $type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;\n $hasRecordingPrompt = false;\n\n if (\n ! $isOutbound\n && ! empty($this->provider->capabilities)\n && isset($this->provider->capabilities['HasRecordingPrompt'])\n ) {\n $hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];\n }\n\n $status = $this->mapStatus($callData['result']);\n $recordingReasonCode = $status === Activity::STATUS_FAILED ?\n Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;\n\n /** @var Activity $activity */\n $activity = $user->activities()->updateOrCreate([\n 'telephony_provider_id' => $callId,\n 'provider' => $this->getName(),\n ], [\n 'type' => $type,\n 'playbook_category_id' => $playbookCategory->id ?? null,\n 'crm_configuration_id' => $this->team->crm_id,\n 'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID\n 'actual_start_time' => $startedAt,\n 'scheduled_start_time' => $startedAt,\n 'actual_end_time' => $completedAt,\n 'scheduled_end_time' => $completedAt,\n 'title' => $task['subject'] ?? $title,\n 'lead_id' => $lead->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'account_id' => $account->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'duration' => $duration,\n 'recording_state' => Activity::RECORDING_RECORDED,\n 'recording_reason_code' => $recordingReasonCode,\n 'is_internal' => false,\n 'description' => $task['summary'] ?? null,\n 'has_recording_prompt' => $hasRecordingPrompt,\n ]);\n\n $this->emitProspectAddedIfNecessary($activity, $this->getName());\n\n $this->logger->info(\n '[RingCentral] Activity finished processing',\n [\n 'activityId' => $activity->id_string,\n 'callId' => $callId,\n 'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',\n ],\n );\n\n $this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);\n $this->updateCrmFieldData($activity, $task, $fields);\n\n $this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);\n $organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);\n $organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);\n $customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);\n $this->createProspectParticipantConnection($callData, $organiserParticipant);\n\n $activity->update([\n 'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,\n 'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,\n ]);\n\n $this->purgeCorruptedTracks($activity);\n\n $track = $this->createTrack($callData, $activity, $organiserConnection);\n\n $this->logger->info('[RingCentral] Dispatching track download event', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'callNeedsUpdate' => $callNeedsUpdate,\n ]);\n\n dispatch(new DownloadTrack(\n trackId: $track->id_string,\n socialAccountId: $this->socialAccount->id,\n debugMode: $this->debugMode || $callNeedsUpdate\n ));\n\n event(new CallImported($activity));\n\n $this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);\n\n return $activity->wasRecentlyCreated || $activity->isDirty();\n }\n\n private function resolveExtensions(Call $call): array\n {\n $extensions = [];\n $foundExtensionIds = [];\n\n foreach (array_reverse($call->getLegs()) as $leg) {\n $extensionId = array_get($leg, 'extension.id');\n\n if ($extensionId === null) {\n continue;\n }\n\n $extensionId = (string) $extensionId;\n\n $hasExtension = $this->getProvider()->providerUsers()\n ->where('external_id', $extensionId)\n ->exists();\n\n if ($hasExtension) {\n $extensions[] = $extensionId;\n } else {\n $foundExtensionIds[] = $extensionId;\n }\n }\n\n if (! empty($foundExtensionIds)) {\n $this->logger->info(\n '[RingCentral] Some extensions were not found in db, resolving from API...',\n ['foundExtensionIds' => $foundExtensionIds]\n );\n\n $foundExtensionIds = array_unique($foundExtensionIds);\n\n foreach ($foundExtensionIds as $extensionId) {\n $remoteUser = $this->resolveExtensionFromApi($extensionId);\n $this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);\n\n if ($remoteUser !== null) {\n $this->logger->info(\"[RingCentral] syncing remote user {$remoteUser->getId()}\");\n $this->syncUserExtension($remoteUser);\n $extensions[] = (string) $remoteUser->getId();\n }\n }\n }\n\n $sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);\n\n // Extension assigned by RingCentral has top priority\n $extensionId = $call->getExtensionId();\n if (! empty($extensionId)) {\n $sortedExtensions->prepend((string) $extensionId);\n }\n\n return $sortedExtensions->unique()->values()->toArray();\n }\n\n private function sortExtensionsByStatus(array $extensions, Call $call): Collection\n {\n $statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];\n $reversedLegs = collect($call->getLegs())->reverse();\n\n return collect($extensions)\n ->map(function ($extension) use ($reversedLegs) {\n $status = $reversedLegs\n ->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)\n ['result'] ?? null;\n\n return ['extension' => $extension, 'result' => $status];\n })\n ->sortBy(function ($item) use ($statusOrder) {\n $index = array_search($item['result'], $statusOrder, true);\n\n return $index !== false ? $index : PHP_INT_MAX;\n })\n ->pluck('extension')\n ->values();\n }\n\n private function resolveExtensionFromApi(string $extensionId): ?Extension\n {\n $this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);\n\n try {\n $record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));\n } catch (Throwable $e) {\n $this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);\n\n return null;\n }\n\n if ($record === null) {\n $this->logger->warning('[RingCentral] extension not found in API');\n\n return null;\n }\n\n if (! is_array($record)) {\n $this->logger->error(\n sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),\n [\n 'type' => gettype($record),\n 'record' => $record,\n ]\n );\n\n return null;\n }\n\n return Extension::createFromPayload($record);\n }\n\n /**\n *\n * @throws InvalidArgumentException\n */\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case 'Unknown':\n case 'Call connected':\n case 'Voicemail':\n case 'Accepted':\n case 'Reply':\n return Activity::STATUS_COMPLETED;\n case 'In Progress':\n return Activity::STATUS_IN_PROGRESS;\n case 'Busy':\n return Activity::STATUS_BUSY;\n case 'Received':\n return Activity::STATUS_RECEIVED;\n case 'No Answer':\n return Activity::STATUS_NO_ANSWER;\n case 'Missed':\n case 'Hang Up':\n case 'Blocked':\n case 'Suspended account':\n case 'Call Failed':\n case 'Call Failure':\n case 'Internal Error':\n case 'IP Phone Offline':\n case 'No Calling Credit':\n case 'Not Allowed':\n case 'Restricted Number':\n case 'Wrong Number':\n case 'Answered Not Accepted':\n case 'Stopped':\n case 'International Disabled':\n case 'International Restricted':\n case 'Abandoned':\n case 'Declined':\n case 'Rejected':\n case 'Fax Receipt Error':\n return Activity::STATUS_FAILED;\n default:\n throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));\n }\n }\n\n private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool\n {\n if ($existingCall === null) {\n return false;\n }\n\n $participants = $existingCall->participants()->get();\n $track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();\n\n $trackRecordingUrls = $this->getTrackRecordingUrls($track);\n $callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());\n\n $reasons = [];\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {\n $reasons[] = 'direction_changed_to_inbound';\n }\n\n if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {\n $reasons[] = 'direction_changed_to_outbound';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {\n $reasons[] = 'missing_callee_participant';\n }\n\n if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {\n $reasons[] = 'missing_caller_participant';\n }\n\n if ($trackRecordingUrls !== $callRecordingUrls) {\n $reasons[] = 'recording_urls_changed';\n }\n\n if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {\n $reasons[] = 'status_changed';\n }\n\n $needsUpdate = ! empty($reasons);\n\n if ($needsUpdate && $shouldLog) {\n $this->logger->info('[RingCentral] Call needs update', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n 'activity' => $existingCall->getUuid(),\n 'reasons' => $reasons,\n 'existingType' => $existingCall->getType(),\n 'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',\n 'existingStatus' => $existingCall->getStatus(),\n 'newStatus' => $call->getStatus(),\n ]);\n }\n\n return $needsUpdate;\n }\n\n private function getCallRecordingUrls(array $callData): array\n {\n $recordingUrls = collect($callData['legs'] ?? [])\n ->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))\n ->map(fn ($leg) => (string) $leg['recording']['contentUri'])\n ->prepend((string) ($callData['recording']['contentUri'] ?? ''))\n ->filter()\n ->unique()\n ->values()\n ->toArray();\n\n return $this->normalizeRecordingUrls($recordingUrls);\n }\n\n private function getTrackRecordingUrls(?Track $track): array\n {\n if ($track === null) {\n return [];\n }\n\n $providerContentUrl = $track->getProviderContentUrl();\n\n if ($providerContentUrl === null || $providerContentUrl === '') {\n return [];\n }\n\n try {\n $decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);\n } catch (JsonException) {\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n if (is_array($decoded)) {\n return $this->normalizeRecordingUrls($decoded);\n }\n\n return $this->normalizeRecordingUrls([$providerContentUrl]);\n }\n\n private function normalizeRecordingUrls(array $urls): array\n {\n $urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));\n $urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));\n sort($urls);\n\n return array_values(array_unique($urls));\n }\n\n public function syncExternalUsers(): Collection\n {\n $this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);\n\n return $this->client\n ->getCompanyExtensions()\n ->map([$this, 'syncUserExtension']);\n }\n\n public function syncUserExtension(Extension $remoteUser): void\n {\n $user = null;\n if ($remoteUser->getContact()->getEmailAddress() !== null) {\n $user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(\n $remoteUser->getContact()->getEmailAddress()\n );\n }\n\n // Extension IDs can move between users so update based on that.\n $this->getProvider()->providerUsers()->updateOrCreate([\n 'external_id' => $remoteUser->getId(),\n ], [\n 'user_id' => $user->id ?? null,\n ]);\n }\n\n /**\n * @throws RingCentralExtensionNotFound\n */\n private function resolveUser(Call $call): ?User\n {\n $this->logger->info('[RingCentral] Resolving user', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n $extensions = $this->resolveExtensions($call);\n\n if (empty($extensions)) {\n $this->logger->warning('[RingCentral] No extension is found', [\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUsers = $this->provider->providerUsers()\n ->whereIn('external_id', $extensions)\n ->whereNotNull('user_id')\n ->orderByRaw(\n 'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',\n $extensions\n )\n ->get();\n\n if ($providerUsers->isEmpty()) {\n $this->logger->warning('[RingCentral] No provider users found for extensions', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n $providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());\n\n if ($providerUser === null) {\n $this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [\n 'extensions' => $extensions,\n 'callId' => $call->getCallId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n return null;\n }\n\n /** @var User $user */\n $user = $providerUser->user;\n\n $this->logger->info('[RingCentral] User found', [\n 'callId' => $call->getCallId(),\n 'userId' => $user->id,\n 'teamId' => $this->team->getId(),\n ]);\n\n return $user;\n }\n\n private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'user_id' => $user->getId(),\n 'email' => $user->getEmailAddress(),\n 'name' => $user->getName(),\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant\n {\n $phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();\n\n return $activity->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'email' => $prospect->email ?? null,\n 'name' => $prospect->name ?? 'Customer',\n 'phone_number' => phone_e164(null, $phoneNumber),\n 'contact_id' => $activity->contact_id ?? null,\n 'lead_id' => $activity->lead_id ?? null,\n 'enter_time' => $activity->actual_start_time,\n 'exit_time' => $activity->actual_end_time,\n ], false);\n }\n\n /**\n * Create participant connection for organiser.\n */\n protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_WEBRTC,\n ], [\n 'direction' => $direction,\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create participant connection for prospect.\n */\n protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection\n {\n $activityRecordingState = $participant->activity->getRecordingState();\n $direction = strtolower($callData['direction']);\n\n /** @var Connection $connection */\n $connection = $participant->connections()->updateOrCreate([\n 'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.\n ], [\n 'direction' => $callData['direction'],\n 'phone' => $participant->phone_number,\n 'opened_at' => $participant->enter_time,\n 'closed_at' => $participant->exit_time,\n 'country_code' => regionCode(null, $participant->phone_number),\n 'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,\n ]);\n\n return $connection;\n }\n\n /**\n * Create and associate track to activity.\n */\n private function createTrack(array $callData, Activity $activity, Connection $connection): Track\n {\n $this->logger->info('[RingCentral] Creating track', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n ]);\n\n $recordingUrls = $this->getCallRecordingUrls($callData);\n\n try {\n $providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);\n } catch (JsonException $exception) {\n $this->logger->warning('[RingCentral] Failed to encode recording urls', [\n 'callId' => $activity->getTelephonyProviderId(),\n 'teamId' => $this->team->getId(),\n 'exception' => $exception->getMessage(),\n ]);\n\n $providerContentUrl = '[]';\n }\n\n /** @var Track $track */\n $track = $activity->tracks()->updateOrCreate([\n 'telephony_provider_id' => $activity->telephony_provider_id,\n ], [\n 'connection_id' => $connection->id,\n 'type' => Track::TYPE_AUDIO,\n 'format' => Track::FORMAT_WAV,\n 'provider_content_url' => $providerContentUrl,\n 'start_time' => $activity->actual_start_time,\n 'end_time' => $activity->actual_end_time,\n ]);\n\n $track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);\n\n return $track;\n }\n\n private function isInternalCall(Call $call): bool\n {\n $callData = $call->getPayload();\n\n $fromExtensionNumber = array_get($callData, 'from.extensionNumber');\n $toExtensionNumber = array_get($callData, 'to.extensionNumber');\n\n return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);\n }\n\n private function saveCallToCache(string $callId, string $cacheType): void\n {\n $timeout = match($cacheType) {\n self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,\n self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,\n default => 0,\n };\n\n Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));\n }\n\n private function isCallInCache(string $callId, string $cacheType): bool\n {\n return Cache::has($this->getCacheKey($callId, $cacheType));\n }\n\n private function getCacheKey(string $callId, string $cacheType): string\n {\n return 'ringcentral:import-call:' . $cacheType . ':' . $callId;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4305373452071923642
|
-1646255984498593726
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
21
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Activity\RingCentral;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use JsonException;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\ImportCall\RingCentral\Call;
use Jiminny\DTO\ImportCall\RingCentral\Extension;
use Jiminny\Events\Activities\Dialers\CallImported;
use Jiminny\Events\Activities\Provider\SyncActivityProviderEvent;
use Jiminny\Exceptions\RingCentralExtensionNotFound;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\RingCentral\DownloadTrack;
use Jiminny\Models\Activity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Services\Activity\ActivityProviderService;
use Jiminny\Services\Activity\RingCentral\Client as RingCentralClient;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class Service extends ActivityProviderService implements ImportsDataInterface
{
private const ENDPOINT_EXTENSION_ITEM = '/restapi/v1.0/account/~/extension/%d';
private const CRM_CALL_ID = 'rcsfl__CALL_UNIQUE_ID__c';
private const CALL_CACHE_CONCURRENCY = 'concurrency';
private const CALL_CACHE_CONCURRENCY_TIMEOUT = 10; // 10 seconds
private const CALL_CACHE_COMPLETED = 'completed';
private const CALL_CACHE_COMPLETED_TIMEOUT = 3600; // 1 hour
protected string $name = Activity::PROVIDER_RINGCENTRAL;
private bool $hasSalesforceIntegrated = false;
private CachedCrmServiceDecorator $cachedCrmServiceDecorator;
public function __construct(?array $config = [])
{
parent::__construct();
$this->client = new RingCentralClient($config);
}
public function getDisplayName(): string
{
return 'RingCentral';
}
public function getInstallationStrategy(): string
{
return self::INSTALLATION_STRATEGY_OAUTH;
}
public function onActivityProviderIntegrated(): void
{
if (! $this->hasProvider()) {
$this->setupActivityProvider();
}
$this->setSocialAccount(null);
$response = $this->client->get('/restapi/v1.0/account/~');
$this->provider->update(['provider_id' => $response['id'] ?? null]);
event(new SyncActivityProviderEvent($this->getProvider(), [
'start_date' => now()->subMonth(),
'end_date' => now(),
]));
}
public function supportedActivityChannels(): array
{
return [
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
];
}
/**
* @throws SocialAccountTokenInvalidException
* @throws \Jiminny\Exceptions\HttpBadRequestException
* @throws \Jiminny\Exceptions\HttpForbiddenException
* @throws \Jiminny\Exceptions\HttpNotFoundException
* @throws \Jiminny\Exceptions\QuotaExceededException
* @throws \Jiminny\Exceptions\ServiceUnavailableException
*/
public function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = null): ?int
{
if ($user !== null) {
$this->logger->warning('User filter is not available with this provider');
}
$this->hasSalesforceIntegrated = $this->team->crm->fields()->where([
'crm_provider_id' => self::CRM_CALL_ID,
'object_type' => 'task',
])->exists();
$this->cachedCrmServiceDecorator = resolve(CachedCrmServiceDecorator::class);
$this->cachedCrmServiceDecorator->setCrmService($this->crmService);
if ($activityId) {
$call = $this->client->getCallLog(['view' => 'Detailed'], $activityId);
if ($call === null) {
return 0;
}
$this->logger->debug('[RingCentral] ApiResponse', $call);
return (int) $this->importCall(Call::createFromPayload($call));
}
/**
* every XX:15, XX:30, and XX:45 we look back 1 hour,
* while at every round hour XX:00 we look back 6 hours
*/
$lookupCandidatePeriod = now()->subHour();
if ($from === null || $from->isAfter($lookupCandidatePeriod)) {
$from = $lookupCandidatePeriod;
}
if ($to === null) {
$to = now();
}
$syncCount = 0;
$offset = 1;
$payload = [
'perPage' => 1000,
'dateFrom' => $from->format(DATE_RFC3339_EXTENDED),
'dateTo' => $to->format(DATE_RFC3339_EXTENDED),
'type' => ['Voice'],
'view' => 'Detailed',
];
do {
$payload['page'] = $offset;
try {
$response = $this->client->getCallLog($payload);
if ($response === null) {
return $syncCount;
}
$this->logger->info(
'[RingCentral] Retrieved call log',
[
'teamId' => $this->team->id_string,
'totalCalls' => count($response['records']),
'payload' => $payload,
]
);
foreach ($response['records'] as $call) {
$ringCentralCall = Call::createFromPayload($call);
try {
$syncCount += (int) $this->importCall($ringCentralCall);
} catch (RingCentralExtensionNotFound $exception) {
$this->logger->warning('RingCentral extension not found', [
'team' => $this->team->id_string,
'extension' => $exception->getExtensionId(),
]);
Sentry::captureException($exception);
}
}
} catch (Throwable $exception) {
$this->logger->error('[RingCentral] Import has failed', [
'team' => $this->team->getSlug(),
'socialAccountId' => $this->socialAccount->getId(),
'code' => $exception->getCode(),
'exception' => $exception->getMessage(),
]);
return $syncCount;
}
$offset++;
} while (isset($response['navigation']['nextPage']));
$this->provider->update(['last_synced_at' => now()]);
return $syncCount;
}
private function importCall(Call $call): bool
{
$callId = $call->getCallId();
if (($this->isCallInCache($callId, self::CALL_CACHE_CONCURRENCY)
|| $this->isCallInCache($callId, self::CALL_CACHE_COMPLETED))
&& ! $this->debugMode
) {
$this->logger->info('[RingCentral] Call already imported', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$this->saveCallToCache($callId, self::CALL_CACHE_CONCURRENCY);
$callData = $call->getPayload();
$isOutbound = $call->isDirectionOutbound();
$playbookCategory = $lead = $account = $opportunity = $contact = $stage = null;
$fields = $task = [];
$this->logger->info('[RingCentral] Syncing call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
'callData' => $callData,
]);
if ($this->isInternalCall($call)) {
$this->logger->info('[RingCentral] Skipping internal call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! $call->isRecordingAvailable()) {
$this->logger->warning('[RingCentral] Call has no recording', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
if (! isset($callData['from'], $callData['to'])) {
$this->logger->warning('[RingCentral] Call is missing from or to data', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$user = $this->resolveUser($call);
if (! $user instanceof User) {
$this->logger->info('[RingCentral] No user found. Skipping call', [
'callId' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$existingCall = $user->activities()
->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
$callNeedsUpdate = $this->callNeedsUpdate($call, $existingCall, shouldLog: $existingCall !== null);
if ($existingCall && ! $callNeedsUpdate && ! $this->debugMode) {
$this->logger->info('[RingCentral] Call already exists', [
'call_id' => $callId,
'teamId' => $this->team->getId(),
]);
return false;
}
$phone = $isOutbound
? $call->getCalleePhoneNumber()
: $call->getCallerPhoneNumber();
$title = 'Call ' . ($isOutbound ? 'to ' : 'from ');
$playbook = $this->getPlaybook($user);
if ($this->hasSalesforceIntegrated) {
try {
$fields = $this->team->crm->fields()->where('is_indexable', 1)
->where('object_type', 'task')->get()->all();
$crmFilters = [
self::CRM_CALL_ID => '%' . $call->getSessionId() . '-%',
];
$task = $this->getTaskByCallId($call->getSessionId(), $playbook->activityField, $crmFilters, $fields);
} catch (Exception $ex) {
Sentry::captureException($ex);
}
}
if (! empty($task) && $this->hasSalesforceIntegrated) {
[$lead, $account, $opportunity, $contact, $stage] = $this->crmService->fetchProspect($task);
if ($task['Type']) {
$playbookCategory = $this->getPlaybookCategory($playbook, $task['Type']);
}
} elseif ($phone !== null) {
// Note in some cases phone might be null.
[$lead, $account, $opportunity, $contact, $stage] = $this->cachedCrmServiceDecorator->matchByPhone(
$phone,
null,
$user->getId()
);
}
if ($lead === null && $contact === null && $account === null) {
$this->logger->info(sprintf('[RingCentral] Contact does not have CRM data for call %s', $callId), [
'person' => $callData['to'],
]);
$title .= $phone;
} else {
$title .= ($lead ? $lead->name : ($contact ? $contact->name : ($account ? $account->name : '')));
}
$startedAt = $call->getActualStartTime();
$completedAt = $call->getActualEndTime();
$duration = $call->getDuration();
/** @var Activity|null $activity */
$activity = $user->activities()->withTrashed()
->where('telephony_provider_id', $callId)
->where('provider', $this->getName())
->first();
if ($activity?->trashed()) {
$this->logger->warning('[RingCentral] Existing activity is soft-deleted', ['callId' => $callId]);
return false;
}
$type = $isOutbound ? Activity::TYPE_SOFTPHONE : Activity::TYPE_SOFTPHONE_INBOUND;
$hasRecordingPrompt = false;
if (
! $isOutbound
&& ! empty($this->provider->capabilities)
&& isset($this->provider->capabilities['HasRecordingPrompt'])
) {
$hasRecordingPrompt = $this->provider->capabilities['HasRecordingPrompt'];
}
$status = $this->mapStatus($callData['result']);
$recordingReasonCode = $status === Activity::STATUS_FAILED ?
Activity::FLAG_RECORDING_REASON_EXTERNALLY_DISABLED : 0;
/** @var Activity $activity */
$activity = $user->activities()->updateOrCreate([
'telephony_provider_id' => $callId,
'provider' => $this->getName(),
], [
'type' => $type,
'playbook_category_id' => $playbookCategory->id ?? null,
'crm_configuration_id' => $this->team->crm_id,
'crm_provider_id' => $task['crmId'] ?? null, // Salesforce Activity ID
'actual_start_time' => $startedAt,
'scheduled_start_time' => $startedAt,
'actual_end_time' => $completedAt,
'scheduled_end_time' => $completedAt,
'title' => $task['subject'] ?? $title,
'lead_id' => $lead->id ?? null,
'contact_id' => $contact->id ?? null,
'account_id' => $account->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'duration' => $duration,
'recording_state' => Activity::RECORDING_RECORDED,
'recording_reason_code' => $recordingReasonCode,
'is_internal' => false,
'description' => $task['summary'] ?? null,
'has_recording_prompt' => $hasRecordingPrompt,
]);
$this->emitProspectAddedIfNecessary($activity, $this->getName());
$this->logger->info(
'[RingCentral] Activity finished processing',
[
'activityId' => $activity->id_string,
'callId' => $callId,
'status' => $activity->wasRecentlyCreated ? 'imported' : 'synced',
],
);
$this->logger->info('[RingCentral] Updating CRM', ['callId' => $call->getCallId()]);
$this->updateCrmFieldData($activity, $task, $fields);
$this->logger->info('[RingCentral] Creating participants', ['callId' => $call->getCallId()]);
$organiserParticipant = $this->createOrganiserParticipant($activity, $user, $call);
$organiserConnection = $this->createOrganiserParticipantConnection($callData, $organiserParticipant);
$customerParticipant = $this->createProspectParticipant($activity, $activity->prospect, $call);
$this->createProspectParticipantConnection($callData, $organiserParticipant);
$activity->update([
'from_participant_id' => $isOutbound ? $organiserParticipant->id : $customerParticipant->id,
'to_participant_id' => $isOutbound ? $customerParticipant->id : $organiserParticipant->id,
]);
$this->purgeCorruptedTracks($activity);
$track = $this->createTrack($callData, $activity, $organiserConnection);
$this->logger->info('[RingCentral] Dispatching track download event', [
'callId' => $activity->getTelephonyProviderId(),
'callNeedsUpdate' => $callNeedsUpdate,
]);
dispatch(new DownloadTrack(
trackId: $track->id_string,
socialAccountId: $this->socialAccount->id,
debugMode: $this->debugMode || $callNeedsUpdate
));
event(new CallImported($activity));
$this->saveCallToCache($callId, self::CALL_CACHE_COMPLETED);
return $activity->wasRecentlyCreated || $activity->isDirty();
}
private function resolveExtensions(Call $call): array
{
$extensions = [];
$foundExtensionIds = [];
foreach (array_reverse($call->getLegs()) as $leg) {
$extensionId = array_get($leg, 'extension.id');
if ($extensionId === null) {
continue;
}
$extensionId = (string) $extensionId;
$hasExtension = $this->getProvider()->providerUsers()
->where('external_id', $extensionId)
->exists();
if ($hasExtension) {
$extensions[] = $extensionId;
} else {
$foundExtensionIds[] = $extensionId;
}
}
if (! empty($foundExtensionIds)) {
$this->logger->info(
'[RingCentral] Some extensions were not found in db, resolving from API...',
['foundExtensionIds' => $foundExtensionIds]
);
$foundExtensionIds = array_unique($foundExtensionIds);
foreach ($foundExtensionIds as $extensionId) {
$remoteUser = $this->resolveExtensionFromApi($extensionId);
$this->logger->info('[RingCentral] fetch result', ['remote_user' => $remoteUser]);
if ($remoteUser !== null) {
$this->logger->info("[RingCentral] syncing remote user {$remoteUser->getId()}");
$this->syncUserExtension($remoteUser);
$extensions[] = (string) $remoteUser->getId();
}
}
}
$sortedExtensions = $this->sortExtensionsByStatus($extensions, $call);
// Extension assigned by RingCentral has top priority
$extensionId = $call->getExtensionId();
if (! empty($extensionId)) {
$sortedExtensions->prepend((string) $extensionId);
}
return $sortedExtensions->unique()->values()->toArray();
}
private function sortExtensionsByStatus(array $extensions, Call $call): Collection
{
$statusOrder = ['Completed', 'Call connected', 'Connected', 'Accepted'];
$reversedLegs = collect($call->getLegs())->reverse();
return collect($extensions)
->map(function ($extension) use ($reversedLegs) {
$status = $reversedLegs
->first(fn ($leg) => isset($leg['extension']['id']) && (string) $leg['extension']['id'] === $extension)
['result'] ?? null;
return ['extension' => $extension, 'result' => $status];
})
->sortBy(function ($item) use ($statusOrder) {
$index = array_search($item['result'], $statusOrder, true);
return $index !== false ? $index : PHP_INT_MAX;
})
->pluck('extension')
->values();
}
private function resolveExtensionFromApi(string $extensionId): ?Extension
{
$this->logger->info('[RingCentral] fetching extension from API', ['extensionId' => $extensionId]);
try {
$record = $this->client->get(sprintf(self::ENDPOINT_EXTENSION_ITEM, $extensionId));
} catch (Throwable $e) {
$this->logger->warning('[RingCentral] API error', ['error' => $e->getMessage()]);
return null;
}
if ($record === null) {
$this->logger->warning('[RingCentral] extension not found in API');
return null;
}
if (! is_array($record)) {
$this->logger->error(
sprintf('[RingCentral] %s:%s wrong variable type', __FILE__, __LINE__),
[
'type' => gettype($record),
'record' => $record,
]
);
return null;
}
return Extension::createFromPayload($record);
}
/**
*
* @throws InvalidArgumentException
*/
private function mapStatus(string $status): string
{
switch ($status) {
case 'Unknown':
case 'Call connected':
case 'Voicemail':
case 'Accepted':
case 'Reply':
return Activity::STATUS_COMPLETED;
case 'In Progress':
return Activity::STATUS_IN_PROGRESS;
case 'Busy':
return Activity::STATUS_BUSY;
case 'Received':
return Activity::STATUS_RECEIVED;
case 'No Answer':
return Activity::STATUS_NO_ANSWER;
case 'Missed':
case 'Hang Up':
case 'Blocked':
case 'Suspended account':
case 'Call Failed':
case 'Call Failure':
case 'Internal Error':
case 'IP Phone Offline':
case 'No Calling Credit':
case 'Not Allowed':
case 'Restricted Number':
case 'Wrong Number':
case 'Answered Not Accepted':
case 'Stopped':
case 'International Disabled':
case 'International Restricted':
case 'Abandoned':
case 'Declined':
case 'Rejected':
case 'Fax Receipt Error':
return Activity::STATUS_FAILED;
default:
throw new InvalidArgumentException(sprintf('Unknown call state: %s', $status));
}
}
private function callNeedsUpdate(Call $call, ?Activity $existingCall, bool $shouldLog = false): bool
{
if ($existingCall === null) {
return false;
}
$participants = $existingCall->participants()->get();
$track = $existingCall->tracks()->whereNotNull('telephony_provider_id')->first();
$trackRecordingUrls = $this->getTrackRecordingUrls($track);
$callRecordingUrls = $this->getCallRecordingUrls($call->getPayload());
$reasons = [];
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE && $call->isDirectionInbound()) {
$reasons[] = 'direction_changed_to_inbound';
}
if ($existingCall->getType() === Activity::TYPE_SOFTPHONE_INBOUND && $call->isDirectionOutbound()) {
$reasons[] = 'direction_changed_to_outbound';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCalleePhoneNumber()))) {
$reasons[] = 'missing_callee_participant';
}
if (! $participants->contains('phone_number', phone_e164(null, $call->getCallerPhoneNumber()))) {
$reasons[] = 'missing_caller_participant';
}
if ($trackRecordingUrls !== $callRecordingUrls) {
$reasons[] = 'recording_urls_changed';
}
if ($existingCall->getStatus() !== $this->mapStatus($call->getStatus())) {
$reasons[] = 'status_changed';
}
$needsUpdate = ! empty($reasons);
if ($needsUpdate && $shouldLog) {
$this->logger->info('[RingCentral] Call needs update', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
'activity' => $existingCall->getUuid(),
'reasons' => $reasons,
'existingType' => $existingCall->getType(),
'newDirection' => $call->isDirectionInbound() ? 'Inbound' : 'Outbound',
'existingStatus' => $existingCall->getStatus(),
'newStatus' => $call->getStatus(),
]);
}
return $needsUpdate;
}
private function getCallRecordingUrls(array $callData): array
{
$recordingUrls = collect($callData['legs'] ?? [])
->filter(fn ($leg) => ! empty($leg['recording']['contentUri']))
->map(fn ($leg) => (string) $leg['recording']['contentUri'])
->prepend((string) ($callData['recording']['contentUri'] ?? ''))
->filter()
->unique()
->values()
->toArray();
return $this->normalizeRecordingUrls($recordingUrls);
}
private function getTrackRecordingUrls(?Track $track): array
{
if ($track === null) {
return [];
}
$providerContentUrl = $track->getProviderContentUrl();
if ($providerContentUrl === null || $providerContentUrl === '') {
return [];
}
try {
$decoded = json_decode($providerContentUrl, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
if (is_array($decoded)) {
return $this->normalizeRecordingUrls($decoded);
}
return $this->normalizeRecordingUrls([$providerContentUrl]);
}
private function normalizeRecordingUrls(array $urls): array
{
$urls = array_values(array_filter($urls, static fn (mixed $url): bool => is_scalar($url) && $url !== ''));
$urls = array_values(array_filter(array_map('strval', $urls), static fn (string $url): bool => $url !== ''));
sort($urls);
return array_values(array_unique($urls));
}
public function syncExternalUsers(): Collection
{
$this->logger->info('[RingCentral] Syncing company extensions', ['team' => $this->team->getSlug()]);
return $this->client
->getCompanyExtensions()
->map([$this, 'syncUserExtension']);
}
public function syncUserExtension(Extension $remoteUser): void
{
$user = null;
if ($remoteUser->getContact()->getEmailAddress() !== null) {
$user = $this->team->findActiveNonCoachesThatSyncDialersByEmail(
$remoteUser->getContact()->getEmailAddress()
);
}
// Extension IDs can move between users so update based on that.
$this->getProvider()->providerUsers()->updateOrCreate([
'external_id' => $remoteUser->getId(),
], [
'user_id' => $user->id ?? null,
]);
}
/**
* @throws RingCentralExtensionNotFound
*/
private function resolveUser(Call $call): ?User
{
$this->logger->info('[RingCentral] Resolving user', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
$extensions = $this->resolveExtensions($call);
if (empty($extensions)) {
$this->logger->warning('[RingCentral] No extension is found', [
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUsers = $this->provider->providerUsers()
->whereIn('external_id', $extensions)
->whereNotNull('user_id')
->orderByRaw(
'FIELD(external_id, ' . implode(',', array_fill(0, count($extensions), '?')) . ')',
$extensions
)
->get();
if ($providerUsers->isEmpty()) {
$this->logger->warning('[RingCentral] No provider users found for extensions', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
$providerUser = $providerUsers->first(fn ($providerUser) => $providerUser->user->shouldSyncDialer());
if ($providerUser === null) {
$this->logger->warning('[RingCentral] Only active recorders should have meetings imported.', [
'extensions' => $extensions,
'callId' => $call->getCallId(),
'teamId' => $this->team->getId(),
]);
return null;
}
/** @var User $user */
$user = $providerUser->user;
$this->logger->info('[RingCentral] User found', [
'callId' => $call->getCallId(),
'userId' => $user->id,
'teamId' => $this->team->getId(),
]);
return $user;
}
private function createOrganiserParticipant(Activity $activity, User $user, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCallerPhoneNumber() : $call->getCalleePhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'user_id' => $user->getId(),
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'phone_number' => phone_e164(null, $phoneNumber),
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
protected function createProspectParticipant(Activity $activity, $prospect, Call $call): Participant
{
$phoneNumber = $call->isDirectionOutbound() ? $call->getCalleePhoneNumber() : $call->getCallerPhoneNumber();
return $activity->updateOrCreateParticipant([
'is_ghost' => 0,
'email' => $prospect->email ?? null,
'name' => $prospect->name ?? 'Customer',
'phone_number' => phone_e164(null, $phoneNumber),
'contact_id' => $activity->contact_id ?? null,
'lead_id' => $activity->lead_id ?? null,
'enter_time' => $activity->actual_start_time,
'exit_time' => $activity->actual_end_time,
], false);
}
/**
* Create participant connection for organiser.
*/
protected function createOrganiserParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_WEBRTC,
], [
'direction' => $direction,
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'outbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create participant connection for prospect.
*/
protected function createProspectParticipantConnection(array $callData, Participant $participant): Connection
{
$activityRecordingState = $participant->activity->getRecordingState();
$direction = strtolower($callData['direction']);
/** @var Connection $connection */
$connection = $participant->connections()->updateOrCreate([
'type' => Connection::TYPE_PSTN, // For the prospect it is always PSTN.
], [
'direction' => $callData['direction'],
'phone' => $participant->phone_number,
'opened_at' => $participant->enter_time,
'closed_at' => $participant->exit_time,
'country_code' => regionCode(null, $participant->phone_number),
'recording_state' => $direction === 'inbound' ? $activityRecordingState : Connection::RECORDING_OFF,
]);
return $connection;
}
/**
* Create and associate track to activity.
*/
private function createTrack(array $callData, Activity $activity, Connection $connection): Track
{
$this->logger->info('[RingCentral] Creating track', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
]);
$recordingUrls = $this->getCallRecordingUrls($callData);
try {
$providerContentUrl = json_encode($recordingUrls, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->logger->warning('[RingCentral] Failed to encode recording urls', [
'callId' => $activity->getTelephonyProviderId(),
'teamId' => $this->team->getId(),
'exception' => $exception->getMessage(),
]);
$providerContentUrl = '[]';
}
/** @var Track $track */
$track = $activity->tracks()->updateOrCreate([
'telephony_provider_id' => $activity->telephony_provider_id,
], [
'connection_id' => $connection->id,
'type' => Track::TYPE_AUDIO,
'format' => Track::FORMAT_WAV,
'provider_content_url' => $providerContentUrl,
'start_time' => $activity->actual_start_time,
'end_time' => $activity->actual_end_time,
]);
$track->wasRecentlyCreated && $track->update(['status' => Track::STATUS_PENDING]);
return $track;
}
private function isInternalCall(Call $call): bool
{
$callData = $call->getPayload();
$fromExtensionNumber = array_get($callData, 'from.extensionNumber');
$toExtensionNumber = array_get($callData, 'to.extensionNumber');
return ! empty($fromExtensionNumber) && ! empty($toExtensionNumber);
}
private function saveCallToCache(string $callId, string $cacheType): void
{
$timeout = match($cacheType) {
self::CALL_CACHE_CONCURRENCY => self::CALL_CACHE_CONCURRENCY_TIMEOUT,
self::CALL_CACHE_COMPLETED => self::CALL_CACHE_COMPLETED_TIMEOUT,
default => 0,
};
Cache::put($this->getCacheKey($callId, $cacheType), true, now()->addSeconds($timeout));
}
private function isCallInCache(string $callId, string $cacheType): bool
{
return Cache::has($this->getCacheKey($callId, $cacheType));
}
private function getCacheKey(string $callId, string $cacheType): string
{
return 'ringcentral:import-call:' . $cacheType . ':' . $callId;
}
}...
|
55282
|
NULL
|
NULL
|
NULL
|
|
55286
|
1915
|
22
|
2026-05-18T13:59:57.138661+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112797138_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIewINavicarecodeLaravelFV faVsco.js°9 master k >Proiect v© ParentNode.php(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.php(c)SmsMessage.pnp© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnectJ Apollo> @J AvayaC BaseService95 Gt› _ bloobirasuCloseCloudcallCloudtalkConnectAndSellD DemoDeskDialpadl sichisvziahFiveNineM GmaillWiminny Services Activitv RinaCentr• Methodm d importData service ...app/services/Activity RinaCen> Usages in Prolect Files 1 result: [Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:56C BaseService.pnp© SoftPhoneManager.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyc ScimProvisioning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php x29 Oclass service extends Activityproviderservice impLements Importsbataintertace mA21 M421 AV340>* acnrows Jiminny.cxcepczons Hcconocroundexcept10n* @throwsJiminny\Exceptions\QuotaExceededException= 444So)* dcnrows Jiminnu excepcions serviceunavazladlecxcepezonublic function importbata@carbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringif (suser == nulusthis->loqger-warnina'User filter 1s not avallable with this provider')isthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oierm orovider id'= self::CRM CALL IDI'object_type' => 'task',1->existso:Е131 ,=155136 @›145public function shouldApplyQueries: boolk...,public function getQueries: FilterDefinitionQueryCollectiont...public function toArray@: arrayt...private function getOptions): array{...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function getValidationRules(?string Sprefix = null): array{...}CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role ManagemeWARN Metadata found iin doc-comment for method9/ 10 tacke done8 tiles/U ListenerkoiecannotHaveAaminurmanagerrermissionkuie.pnp +39apo/Comoonent/SClM/=Constants.ono +e/ u coreUser.phpapp/Component/SCIM/Mutators/Attributes/User/® RoleAttr.php +171ites/User/ RoleAttrTest.php +224ann/DTO/SCIMIAAD/Request/MCoreUserRequest.ohn t15Ask anvthina (*4D1+ <> Code SWE-1.6+0 ..inp on LineView all* Reject allAccent alllActivity:: TYPE_SOFTPHONE.Activity:: TYPE SOFTPHONE INBOUND.95@* @throws SocialAccountTokenInvalidexception* othrows Viminnu Exceotions HttoBadRequestExceptionathrows Jiminny Exceptions HttpForbiddenException• Othrows Waimannu ExcentionsHttoNotFoundExcentzonOthrows Jiminny Exceptions QuotaExceededExceptionAthrows Waiminnu ExcentionsServicellnavalahleExcentionpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = nulU): ?intif (Sucen 1== null)d$this->logger->warning('User filter is not available with this provider');$this->hasSalesforceIntegrated = Sthis->team->crm->fields->where(['crm provider id' => self::CRM_CALL ID'object type' =>])->exists0:Sthis->cachedCrmServiceDecorator = resolve( name: CachedCrmServiceDecorator::class):wweAdAhodieamConthoic fdantaudeuta)hCall HierarchyW Windsurf Toams 05-21/10 charc) UTF.8 Al A enasoe...
|
NULL
|
1787670353670906332
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIewINavicarecodeLaravelFV faVsco.js°9 master k >Proiect v© ParentNode.php(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.php(c)SmsMessage.pnp© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnectJ Apollo> @J AvayaC BaseService95 Gt› _ bloobirasuCloseCloudcallCloudtalkConnectAndSellD DemoDeskDialpadl sichisvziahFiveNineM GmaillWiminny Services Activitv RinaCentr• Methodm d importData service ...app/services/Activity RinaCen> Usages in Prolect Files 1 result: [Platform] Refinemen... 1m left100% 2• Mon 18 May 16:59:56C BaseService.pnp© SoftPhoneManager.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyc ScimProvisioning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php x29 Oclass service extends Activityproviderservice impLements Importsbataintertace mA21 M421 AV340>* acnrows Jiminny.cxcepczons Hcconocroundexcept10n* @throwsJiminny\Exceptions\QuotaExceededException= 444So)* dcnrows Jiminnu excepcions serviceunavazladlecxcepezonublic function importbata@carbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringif (suser == nulusthis->loqger-warnina'User filter 1s not avallable with this provider')isthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oierm orovider id'= self::CRM CALL IDI'object_type' => 'task',1->existso:Е131 ,=155136 @›145public function shouldApplyQueries: boolk...,public function getQueries: FilterDefinitionQueryCollectiont...public function toArray@: arrayt...private function getOptions): array{...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function getValidationRules(?string Sprefix = null): array{...}CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role ManagemeWARN Metadata found iin doc-comment for method9/ 10 tacke done8 tiles/U ListenerkoiecannotHaveAaminurmanagerrermissionkuie.pnp +39apo/Comoonent/SClM/=Constants.ono +e/ u coreUser.phpapp/Component/SCIM/Mutators/Attributes/User/® RoleAttr.php +171ites/User/ RoleAttrTest.php +224ann/DTO/SCIMIAAD/Request/MCoreUserRequest.ohn t15Ask anvthina (*4D1+ <> Code SWE-1.6+0 ..inp on LineView all* Reject allAccent alllActivity:: TYPE_SOFTPHONE.Activity:: TYPE SOFTPHONE INBOUND.95@* @throws SocialAccountTokenInvalidexception* othrows Viminnu Exceotions HttoBadRequestExceptionathrows Jiminny Exceptions HttpForbiddenException• Othrows Waimannu ExcentionsHttoNotFoundExcentzonOthrows Jiminny Exceptions QuotaExceededExceptionAthrows Waiminnu ExcentionsServicellnavalahleExcentionpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $activityId = nulU): ?intif (Sucen 1== null)d$this->logger->warning('User filter is not available with this provider');$this->hasSalesforceIntegrated = Sthis->team->crm->fields->where(['crm provider id' => self::CRM_CALL ID'object type' =>])->exists0:Sthis->cachedCrmServiceDecorator = resolve( name: CachedCrmServiceDecorator::class):wweAdAhodieamConthoic fdantaudeuta)hCall HierarchyW Windsurf Toams 05-21/10 charc) UTF.8 Al A enasoe...
|
55283
|
NULL
|
NULL
|
NULL
|
|
55285
|
1914
|
39
|
2026-05-18T13:59:57.151436+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112797151_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles• 0→Too FirefoxFileEditViewHistoryBookmarksProfiles• 0→ToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left100% C8 • Mon 18 May 16:59:575Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:46:24...
|
NULL
|
-4965539153863625911
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles• 0→Too FirefoxFileEditViewHistoryBookmarksProfiles• 0→ToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left100% C8 • Mon 18 May 16:59:575Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement •1:46:24...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55287
|
1914
|
40
|
2026-05-18T14:00:00.195282+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112800195_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity/RingCentral/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left)100% C8 • Mon 18 May 17:00:0040 5010•j•Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement ®1:46:27...
|
NULL
|
-2338268851592535955
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com[Platform] Refinemen... 1 m left)100% C8 • Mon 18 May 17:00:0040 5010•j•Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik4:59 PM | [Platform] Refinement ®1:46:27...
|
55285
|
NULL
|
NULL
|
NULL
|
|
55288
|
1915
|
23
|
2026-05-18T14:00:00.927132+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112800927_m2.jpg...
|
iTerm2
|
NULL
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIewINavicarecodeLaravelFV faVsco.js°9 master kroledey© ParentNode.php(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.phpc)SmsMessage.one© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnect_Apollo› DJ AvayaC BaseService95 Gt› _ bloobirasuCloseu ClouccallCloudtalkConnectAndSellD DemoDeskDialpad200l sichisvziahFiveNineM GmailWiminny Services Activitv RinaCentr• Mothadl(m d importData service ...app/services/Activity RinaCentra• Usages in Prolect Files 1 resulMethod call 1 resultv Mapp 1 resultv D app/Jobs/Activity 1 resultv (C) Activitv/SvncActivitv.oho 1 resultC BaseService.pnp© SoftPhoneManager.php= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyscimProvistoning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php x29 Oclass service extends Activityproviderservice impLements Importsbatalntertace mA21 M4 M1AV340>* acnrows. Jiminny.cxcepczons Heconocroundexcept10r= 44* acnrows Jiminny.cxcepczons uuocacxceededexcepc10n4501)* dcnrows Jiminnu excepcions serviceunavazladlecxcepezonublic function importbata@carbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringif (suser == nulusthis->loqger-warning('User filter is not available with this provider')sthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oierm orovider id'E131 ,—135136 C >'object_type' => 'task',1->existso:a.. fetpublic function shouldApplyQueries: boolk...,public function getQueries: FilterDefinitionQueryCollectiont...public function toArray@: arrayt...private function getOptions): array{...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function getValidationRules(?string Sprefix = null): array{...}: [Platform] Refinemen... 1m left100% 2• Mon 18 May 17:00:00CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role ManagemeWARN Metadata found iin doc-comment for method9/ 10 tacke donees/ U LIstenerkoleCannotHaveAaminurmanagerrermissionkuie.pnp +3gapp/Comoonent/SCIM/=Constants.ono +3se/ b coreUser.phpann/Comnonent/SCIM/Mutatore/Attributes/Ucer/M PoleAtr.nbo 4171tes/User/ D RoleAttrTest.php +224ann/DTO/SCIMIAAD/Request/MCoreUserRequest.ohn t15Ask anvthina (*4L1_ @sCodo CWC.1A+0 ..inp on LineView all* Reject allAccent alllSservice->setDebua(Sthis->imoort->runsinDebuaobSthis-›service = Sservice:nnivate function nuno• Activitvimnon+Recul +Sthis-sloagen-sinfor*/SvncActivitvl Stanti Sthis->conteyt).$this->activityImportManager->start($this->import):SimportedRecords = $this->service->importData($this->import->getStartDateOl$this->import->getEndDateOSthis->userRepository->findOneBy(['id' => Sthis->import->getUserIdOl).Sthis->import->qetActivitvIdoreturn (new Activ1tvimportResulton->setTotal(SimportedRecords)->addImported(SimportedRecords)private function complete(ActivityImportResult Sresult): voidSthis-›activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog: : increment('jiminny.activity.sync.success', 1.0, [Call HierarchyW Windsurf Toams 05.21/10 charc) UTF.8io 4 spaces ©...
|
NULL
|
-7023890491677178725
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIewINavicarecodeLaravelFV faVsco.js°9 master kroledey© ParentNode.php(C) CoreUserRequest.onp© RolesAndCrmOptionMismatchRule.phpc)SmsMessage.one© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnect_Apollo› DJ AvayaC BaseService95 Gt› _ bloobirasuCloseu ClouccallCloudtalkConnectAndSellD DemoDeskDialpad200l sichisvziahFiveNineM GmailWiminny Services Activitv RinaCentr• Mothadl(m d importData service ...app/services/Activity RinaCentra• Usages in Prolect Files 1 resulMethod call 1 resultv Mapp 1 resultv D app/Jobs/Activity 1 resultv (C) Activitv/SvncActivitv.oho 1 resultC BaseService.pnp© SoftPhoneManager.php= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyscimProvistoning.ong© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~public tunction -_construct(Userrepository suserkepository)l...© Activity/Close/service.pnp© Activity/RingCentral/Service.php x29 Oclass service extends Activityproviderservice impLements Importsbatalntertace mA21 M4 M1AV340>* acnrows. Jiminny.cxcepczons Heconocroundexcept10r= 44* acnrows Jiminny.cxcepczons uuocacxceededexcepc10n4501)* dcnrows Jiminnu excepcions serviceunavazladlecxcepezonublic function importbata@carbon Sfrom = nulu, ?carbon Sto = nulu. ?User suser = null, ?stringif (suser == nulusthis->loqger-warning('User filter is not available with this provider')sthas->hassalesforceinteorated = sthis->team->crm->Freldso ->where oierm orovider id'E131 ,—135136 C >'object_type' => 'task',1->existso:a.. fetpublic function shouldApplyQueries: boolk...,public function getQueries: FilterDefinitionQueryCollectiont...public function toArray@: arrayt...private function getOptions): array{...}public function getValue: arrayi...}private function getDefaultValue(): array{...}public function getValidationRules(?string Sprefix = null): array{...}: [Platform] Refinemen... 1m left100% 2• Mon 18 May 17:00:00CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role ManagemeWARN Metadata found iin doc-comment for method9/ 10 tacke donees/ U LIstenerkoleCannotHaveAaminurmanagerrermissionkuie.pnp +3gapp/Comoonent/SCIM/=Constants.ono +3se/ b coreUser.phpann/Comnonent/SCIM/Mutatore/Attributes/Ucer/M PoleAtr.nbo 4171tes/User/ D RoleAttrTest.php +224ann/DTO/SCIMIAAD/Request/MCoreUserRequest.ohn t15Ask anvthina (*4L1_ @sCodo CWC.1A+0 ..inp on LineView all* Reject allAccent alllSservice->setDebua(Sthis->imoort->runsinDebuaobSthis-›service = Sservice:nnivate function nuno• Activitvimnon+Recul +Sthis-sloagen-sinfor*/SvncActivitvl Stanti Sthis->conteyt).$this->activityImportManager->start($this->import):SimportedRecords = $this->service->importData($this->import->getStartDateOl$this->import->getEndDateOSthis->userRepository->findOneBy(['id' => Sthis->import->getUserIdOl).Sthis->import->qetActivitvIdoreturn (new Activ1tvimportResulton->setTotal(SimportedRecords)->addImported(SimportedRecords)private function complete(ActivityImportResult Sresult): voidSthis-›activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog: : increment('jiminny.activity.sync.success', 1.0, [Call HierarchyW Windsurf Toams 05.21/10 charc) UTF.8io 4 spaces ©...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55289
|
1914
|
41
|
2026-05-18T14:00:02.364184+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112802364_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFile• 0EditViewHistory→BookmarksProfilesToo FirefoxFile• 0EditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% C8 • Mon 18 May 17:00:025Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:46:29...
|
NULL
|
-8730374534583533196
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFile• 0EditViewHistory→BookmarksProfilesToo FirefoxFile• 0EditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% C8 • Mon 18 May 17:00:025Galya DimitrovaNikolay YankovNikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:46:29...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55290
|
1915
|
24
|
2026-05-18T14:00:04.837946+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112804837_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorFV faVsco. PhostormVIewINavicarecodeLaravelKeractorFV faVsco.js°9 master k >Proiect v© SoftPhoneManager.phpYC) ParentNode.ohrC) CoreUserRequest.pnp© CoreUser.php© RolesAndCrmOptionMismatchRule.phpc)SmsMessage.one© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnect0 Apollo> @J AvayaC BaseService>_ bloobirasu Closea CloudcallD CloudTalkConnectAndSellD DemoDeskDialpadM FightBvFiahtFiveNineM GmaiD Gong• GoogleMeetGoToMeetingM GrooveM LuhSootImportJiminnyD JustCallMigratorh Natterhay0 Office0 Orumm Outreach• C RingCentral© Client.phpC) Service.php© ThrottleHandler.php> C RingCentralVideoC7 Salesforce• Sales oftTalkdeskM TeamsTelusM TwilidM TwilioflexDirectM TwilioVidenM Uinloade"N VonaadM Xanti• M7oom© Activity/RingCentral/Service.phpsyncacuivity.onp568898class Syncactivity extends Job 1mpLements Shouldoueue165*dchrows Actzvitu?rovzderzxceotzon*dchrows SoczalAccountloken.nva.dzxcentzon* dthrows containerExceotioninterface• dthrows NotFoundExcentioninterfaceActivityImportManager SactivityImportManager,ActivityProviderRegistry $activityProviderRegistry.UserRepository SuserRepository,SentryClient $sentry.LoggerInterface Slogger,): void {...}private function run: ActivityImportResultSthis->logger->info('[SyncActivity] Start'. Sthis->context):c+hic-sactivitvTmnortManager->start(Sth1s->1mporc)SimportedRecords•vice-›importData(sth1s->import->qetstartbateohnthns->imoort->qetznouateoiothas->userkenository->rindunesvu'1d' => sth1s->1moort->cetuserdooSthis->import->qetActivitvIdoreturn inew ActivitvimoortResultool->addImnortedcsimnortedRecords)private function complete(ActivityImportResult $result): void(...}private function failImport(Throwable Sexception): void{...}100% Lz• Mon 18 May 17:00:04AskJiminnyReportActivityServiceTest v= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]CascadeA console [STAGING]© CoachingFeedbackCoachUserln.php XstoheA1A Ydeclarelscrict_cypes=1)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 Gt ›146 @t>150151 C1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}lleanos ofl liminnvlc...
|
NULL
|
-6134989073001016334
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorFV faVsco. PhostormVIewINavicarecodeLaravelKeractorFV faVsco.js°9 master k >Proiect v© SoftPhoneManager.phpYC) ParentNode.ohrC) CoreUserRequest.pnp© CoreUser.php© RolesAndCrmOptionMismatchRule.phpc)SmsMessage.one© TransferUserAdminRule.phpv D Servicesv @ Activity>@ Aircall>@ AmazonConnect0 Apollo> @J AvayaC BaseService>_ bloobirasu Closea CloudcallD CloudTalkConnectAndSellD DemoDeskDialpadM FightBvFiahtFiveNineM GmaiD Gong• GoogleMeetGoToMeetingM GrooveM LuhSootImportJiminnyD JustCallMigratorh Natterhay0 Office0 Orumm Outreach• C RingCentral© Client.phpC) Service.php© ThrottleHandler.php> C RingCentralVideoC7 Salesforce• Sales oftTalkdeskM TeamsTelusM TwilidM TwilioflexDirectM TwilioVidenM Uinloade"N VonaadM Xanti• M7oom© Activity/RingCentral/Service.phpsyncacuivity.onp568898class Syncactivity extends Job 1mpLements Shouldoueue165*dchrows Actzvitu?rovzderzxceotzon*dchrows SoczalAccountloken.nva.dzxcentzon* dthrows containerExceotioninterface• dthrows NotFoundExcentioninterfaceActivityImportManager SactivityImportManager,ActivityProviderRegistry $activityProviderRegistry.UserRepository SuserRepository,SentryClient $sentry.LoggerInterface Slogger,): void {...}private function run: ActivityImportResultSthis->logger->info('[SyncActivity] Start'. Sthis->context):c+hic-sactivitvTmnortManager->start(Sth1s->1mporc)SimportedRecords•vice-›importData(sth1s->import->qetstartbateohnthns->imoort->qetznouateoiothas->userkenository->rindunesvu'1d' => sth1s->1moort->cetuserdooSthis->import->qetActivitvIdoreturn inew ActivitvimoortResultool->addImnortedcsimnortedRecords)private function complete(ActivityImportResult $result): void(...}private function failImport(Throwable Sexception): void{...}100% Lz• Mon 18 May 17:00:04AskJiminnyReportActivityServiceTest v= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]CascadeA console [STAGING]© CoachingFeedbackCoachUserln.php XstoheA1A Ydeclarelscrict_cypes=1)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 Gt ›146 @t>150151 C1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}lleanos ofl liminnvlc...
|
55288
|
NULL
|
NULL
|
NULL
|
|
55291
|
1915
|
25
|
2026-05-18T14:00:08.148969+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112808148_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormProjectFV faVsco.jsVIewg9 master k -INavic PhpStormProjectFV faVsco.jsVIewg9 master k -INavicarecodeLaravelRefactorTOOIS© ReindexForUserJob.phpketуAcuvilysyncJob.onosyncacuivity.ongleardownstream.onpD AiAutomation_A keDorsD AudioAutomatedReports107C) RequestgenerateAskJiminnyReportJob.c108C) RequestgenerateReportJob.pho109© SendReportExpiringSoonMailJob.php110c) SendReportJob.php111© SendReportMailJob.phpc) SendReoortNotGeneratedMail.loo.ond> M Calendarv 17Cmm> O Delete> M HubsootSalesforce(c) Autoloabelavedtocrm.nhnl© CheckAndRetryRemoteMatch.php© CreateFollowupActivity.php© CreateNotes.php© MatchActivitiesToNewOpportunity.php© MatchActivityCrmData.php® NoteObject.php© SaveActivity.php112113114115116117118>1246147148149150151© SaveTranscription.php© SetupLayout.php© SyncActivity.php© SyncFieldMetadata.php© SyncHubspotObjects.php© SyncLeads.php© SyncObjects.php@ SvncOpportunities.Job.ohv© SyncOpportunity.phpC) SvncProfileMetadata.oho©SyncTeamFieldsJob.phpC) SvncTeamMetadata.oho©UpdateOpportunitySpecifications.phpC) UndateStade.oholM DealRisksM Mailboy165188® ActivityController.php© CoreUserRequest.php© SoftPhoneManager.php© CoreUser.php© Activity/RingCentral/Service.phpSyncactivity.onp xccrm/close/service.ohoclass Syncactivity extends Job 1mpLements Shouldoueue* @throws ActivityProviderException* @throws SocialAccountTokenInvalidException* Othrows ContainerExceptionInterface* @throws NotFoundExceptionInterfaceê | Analyzing...N MiddlewardStreamingIM TeamTelephonymlisor© ChangeEmailJob.phpDeactivateUserJob.phpprivate function init(ActivityImportManager $activityImportManager,ActivityProviderRegistry $activityProviderRegistry,UserRepository $userRepository,SentryClient $sentry,LoggerIntertace >Logger,): void f.7private function run(): ActivityImportResult$this->logger->info('[SyncActivity] Start', $this->context);$this->activityImportManager->start($this->import);SimportedRecords = Sthis->service-importData(sthis->imoort->qetstartlateo.sthis->userRenosi.torv->findoneßvarid' = sthis-simoort->oetiser1d0.DrSthis->import->getActivityId()return (new ActivityImportResult())->setTotal($importedRecords)->addImported($importedRecords)private function complete(ActivityImportResult $result): voidf...,private function failImport(Throwable Sexception): voidf….,Il Pollback II Confiaure (todav 14-19)=custom.logA console [STAGING]stoheE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]A console (EU]declare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 € >146 @ >150151 G>1561usadeorivate const int No GrOUp 1d = 999-private UserRepository $userRepository;public function __construct(UserRepository $userRepository){.}public function shouldApplyQueries(): boolf...}public function getQueries(): FilterDefinitionQueryCollectionf...+public function toArray(): arrayf.,private function getOptions(): arrayf...,public function getValue(): arrayf..,private function getDefaultValue(): arrayf...,public function getValidationRules(?string $prefix = null): arrayf…..,public function getSortOrder(): intf...}public function shouldßeIncluded(Team $team): b00lf...}llcanos of l liminnulc100% 28• Mon 18 May 17:00:08CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..php on LineWARN Metadata found in doc-comment for methodw9 / 10 tasks done :• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META_ROLES and USER_PATCH_ATTR_ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintegration.8 Hes with chingon rapp/Component/SClM/ Constants.php +3app/Component/SCIM/ ScimProvisioning.php +85-15nse/D CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.nhn t17ites/Userl ẞ RoleAttrTest.ohn +224Ask anvthina (&4-L)* Reiect alliiAccent allWN Windsurf Teams152•14UTS.8f 4 spaces...
|
NULL
|
-5337550897012830256
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormProjectFV faVsco.jsVIewg9 master k -INavic PhpStormProjectFV faVsco.jsVIewg9 master k -INavicarecodeLaravelRefactorTOOIS© ReindexForUserJob.phpketуAcuvilysyncJob.onosyncacuivity.ongleardownstream.onpD AiAutomation_A keDorsD AudioAutomatedReports107C) RequestgenerateAskJiminnyReportJob.c108C) RequestgenerateReportJob.pho109© SendReportExpiringSoonMailJob.php110c) SendReportJob.php111© SendReportMailJob.phpc) SendReoortNotGeneratedMail.loo.ond> M Calendarv 17Cmm> O Delete> M HubsootSalesforce(c) Autoloabelavedtocrm.nhnl© CheckAndRetryRemoteMatch.php© CreateFollowupActivity.php© CreateNotes.php© MatchActivitiesToNewOpportunity.php© MatchActivityCrmData.php® NoteObject.php© SaveActivity.php112113114115116117118>1246147148149150151© SaveTranscription.php© SetupLayout.php© SyncActivity.php© SyncFieldMetadata.php© SyncHubspotObjects.php© SyncLeads.php© SyncObjects.php@ SvncOpportunities.Job.ohv© SyncOpportunity.phpC) SvncProfileMetadata.oho©SyncTeamFieldsJob.phpC) SvncTeamMetadata.oho©UpdateOpportunitySpecifications.phpC) UndateStade.oholM DealRisksM Mailboy165188® ActivityController.php© CoreUserRequest.php© SoftPhoneManager.php© CoreUser.php© Activity/RingCentral/Service.phpSyncactivity.onp xccrm/close/service.ohoclass Syncactivity extends Job 1mpLements Shouldoueue* @throws ActivityProviderException* @throws SocialAccountTokenInvalidException* Othrows ContainerExceptionInterface* @throws NotFoundExceptionInterfaceê | Analyzing...N MiddlewardStreamingIM TeamTelephonymlisor© ChangeEmailJob.phpDeactivateUserJob.phpprivate function init(ActivityImportManager $activityImportManager,ActivityProviderRegistry $activityProviderRegistry,UserRepository $userRepository,SentryClient $sentry,LoggerIntertace >Logger,): void f.7private function run(): ActivityImportResult$this->logger->info('[SyncActivity] Start', $this->context);$this->activityImportManager->start($this->import);SimportedRecords = Sthis->service-importData(sthis->imoort->qetstartlateo.sthis->userRenosi.torv->findoneßvarid' = sthis-simoort->oetiser1d0.DrSthis->import->getActivityId()return (new ActivityImportResult())->setTotal($importedRecords)->addImported($importedRecords)private function complete(ActivityImportResult $result): voidf...,private function failImport(Throwable Sexception): voidf….,Il Pollback II Confiaure (todav 14-19)=custom.logA console [STAGING]stoheE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]A console (EU]declare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 € >146 @ >150151 G>1561usadeorivate const int No GrOUp 1d = 999-private UserRepository $userRepository;public function __construct(UserRepository $userRepository){.}public function shouldApplyQueries(): boolf...}public function getQueries(): FilterDefinitionQueryCollectionf...+public function toArray(): arrayf.,private function getOptions(): arrayf...,public function getValue(): arrayf..,private function getDefaultValue(): arrayf...,public function getValidationRules(?string $prefix = null): arrayf…..,public function getSortOrder(): intf...}public function shouldßeIncluded(Team $team): b00lf...}llcanos of l liminnulc100% 28• Mon 18 May 17:00:08CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..php on LineWARN Metadata found in doc-comment for methodw9 / 10 tasks done :• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META_ROLES and USER_PATCH_ATTR_ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintegration.8 Hes with chingon rapp/Component/SClM/ Constants.php +3app/Component/SCIM/ ScimProvisioning.php +85-15nse/D CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.nhn t17ites/Userl ẞ RoleAttrTest.ohn +224Ask anvthina (&4-L)* Reiect alliiAccent allWN Windsurf Teams152•14UTS.8f 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55293
|
1915
|
26
|
2026-05-18T14:00:18.572327+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112818572_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelRefactorFV faVsco. PhostormVIewINavicarecodeLaravelRefactorFV faVsco.js°9 master kProiect vCActivityController.ongC BaseService.onp© SoftPhoneManager.phpc) ReindexForUserJob.one(C) CoreUserRequest.onp© CoreUser.phpketуAcuvilysyncJob.onosyncacuivity.ongleardownstream.onpD AiAutomation© Activity/RingCentral/Service.phpSyncactivity.onp xccrm/close/service.oho_A keDorsD AudioAutomatedReportsclass Syncactivity extends Job 1mpLements ShouldoueueC) RequestgenerateAskJiminnyReportJob.c108C) RequestgenerateReportJob.pho© SendReportExpiringSoonMailJob.phpc) SendReportJob.php© SendReportMailJob.phpc) SendReoortNotGeneratedMail.loo.ond> M Calendarv 17Cmm› M Delete> M HubsootM Salesforce(c) Autoloabelavedtocrm.nhnl© CheckAndRetryRemoteMatch.php© CreateFollowupActivity.php© CreateNotes.php(@) MatchActivitiesToNewopportunity.ong(e) MatchActivitvermData.onpe Noteuoectono© SaveActivity.php(e) SaveTranscriotion.onp© SetupLayout.php© SyncActivity.php© SyncFieldMetadata.php© SyncHubspotObjects.php© SyncLeads.php@ SyncObiects.phpc) suncepoortunities.ob.onv@ SvncOpportunitv.phrC) SvncProfileMetadata.oho@ SvncTeamFields.Job.phpC) SvncTeamMetadata.ohv© [EMAIL]) UndateStade.oholM DealRisksM MailboyN MiddlewardD StreamingIM Teamn Tolanhony.mlisor(e) ChanadSmail lnh nhnC DeactivateUser.Job.php110112113E E118146147148149150151165* dchrows Actzvitu?rovzderzxceotzon*dchrows Soczal.ccountloken.nva.dzxcentzon* dthrows ContainerExceotioninterface* dthrows NotFoundExceotionInterfacelusageorivate function init!ActivityImportManager SactivityImportManager,ActivityProviderRegistry SactivityProviderRegistry,UserRepository SuserRepository,SentryClient $sentry.LoggerInterface Slogger,): void {...}private function run: ActivityImportResultsth1s->Loqger->1nto(syncactivity start', sthis->contextSthis-›activityImportManager->start($this->import):SimportedRecords = Sthis->service->importData(sth1s->import->qetstartbateohnthns->imoort->qetznouateoisthas->userreposttorv->r1ndunesvur'1d' => sth1s->1moort->cetuserd00.Sthis->import->qetActivitvIdoreturn inew ActivitvimoortResultool->addImnortedcsimnortedRecords)private function complete(ActivityImportResult $result): void(...}s usagesprivate function failImport(Throwable Sexception): void{...}= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 Gt ›146 @t>150151 G>156usadeorivate const int No GROUP 10 = 999orivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollectionf...hpublic function toArray@: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}100% 1• Mon 18 May 17:00:18SCIM Role Manageme+0 ..inp on LinewCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.ohn t17ites/User/ ẞ RoleAttrTest.nhn +224Ask anvthina (&4-L)llcanos of l liminnulc* Reiect alliiWN Windsurf Toams 152-11UTF.8Accent alliio 4 spaces...
|
NULL
|
-8402452708705605319
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelRefactorFV faVsco. PhostormVIewINavicarecodeLaravelRefactorFV faVsco.js°9 master kProiect vCActivityController.ongC BaseService.onp© SoftPhoneManager.phpc) ReindexForUserJob.one(C) CoreUserRequest.onp© CoreUser.phpketуAcuvilysyncJob.onosyncacuivity.ongleardownstream.onpD AiAutomation© Activity/RingCentral/Service.phpSyncactivity.onp xccrm/close/service.oho_A keDorsD AudioAutomatedReportsclass Syncactivity extends Job 1mpLements ShouldoueueC) RequestgenerateAskJiminnyReportJob.c108C) RequestgenerateReportJob.pho© SendReportExpiringSoonMailJob.phpc) SendReportJob.php© SendReportMailJob.phpc) SendReoortNotGeneratedMail.loo.ond> M Calendarv 17Cmm› M Delete> M HubsootM Salesforce(c) Autoloabelavedtocrm.nhnl© CheckAndRetryRemoteMatch.php© CreateFollowupActivity.php© CreateNotes.php(@) MatchActivitiesToNewopportunity.ong(e) MatchActivitvermData.onpe Noteuoectono© SaveActivity.php(e) SaveTranscriotion.onp© SetupLayout.php© SyncActivity.php© SyncFieldMetadata.php© SyncHubspotObjects.php© SyncLeads.php@ SyncObiects.phpc) suncepoortunities.ob.onv@ SvncOpportunitv.phrC) SvncProfileMetadata.oho@ SvncTeamFields.Job.phpC) SvncTeamMetadata.ohv© [EMAIL]) UndateStade.oholM DealRisksM MailboyN MiddlewardD StreamingIM Teamn Tolanhony.mlisor(e) ChanadSmail lnh nhnC DeactivateUser.Job.php110112113E E118146147148149150151165* dchrows Actzvitu?rovzderzxceotzon*dchrows Soczal.ccountloken.nva.dzxcentzon* dthrows ContainerExceotioninterface* dthrows NotFoundExceotionInterfacelusageorivate function init!ActivityImportManager SactivityImportManager,ActivityProviderRegistry SactivityProviderRegistry,UserRepository SuserRepository,SentryClient $sentry.LoggerInterface Slogger,): void {...}private function run: ActivityImportResultsth1s->Loqger->1nto(syncactivity start', sthis->contextSthis-›activityImportManager->start($this->import):SimportedRecords = Sthis->service->importData(sth1s->import->qetstartbateohnthns->imoort->qetznouateoisthas->userreposttorv->r1ndunesvur'1d' => sth1s->1moort->cetuserd00.Sthis->import->qetActivitvIdoreturn inew ActivitvimoortResultool->addImnortedcsimnortedRecords)private function complete(ActivityImportResult $result): void(...}s usagesprivate function failImport(Throwable Sexception): void{...}= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 Gt ›146 @t>150151 G>156usadeorivate const int No GROUP 10 = 999orivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollectionf...hpublic function toArray@: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}100% 1• Mon 18 May 17:00:18SCIM Role Manageme+0 ..inp on LinewCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.ohn t17ites/User/ ẞ RoleAttrTest.nhn +224Ask anvthina (&4-L)llcanos of l liminnulc* Reiect alliiWN Windsurf Toams 152-11UTF.8Accent alliio 4 spaces...
|
55291
|
NULL
|
NULL
|
NULL
|
|
55292
|
1914
|
42
|
2026-05-18T14:00:18.659032+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112818659_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% <78 • Mon 18 May 17:00:18)5Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:46:45...
|
NULL
|
-5637701404343316258
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% <78 • Mon 18 May 17:00:18)5Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:46:45...
|
55289
|
NULL
|
NULL
|
NULL
|
|
55294
|
1914
|
43
|
2026-05-18T14:00:24.118538+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112824118_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08722222,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5","depth":22,"bounds":{"left":0.9145833,"top":0.09888889,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.08833333,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"bounds":{"left":0.26145834,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"bounds":{"left":0.2892361,"top":0.31,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Galya Dimitrova","depth":13,"bounds":{"left":0.31979167,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.10729167,"top":0.48277777,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-5895609627623219995
|
-141203165865289082
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
FirefoxFileEdit→ViewHistoryBookmarksProfilesCToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% [8• Mon 18 May 17:00:23=A05Galya DimitrovaNikolay Yankov*4Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:46:50...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55295
|
1915
|
27
|
2026-05-18T14:00:24.148385+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112824148_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.69481385,"top":1.0,"width":0.019614361,"height":-0.06264961},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5","depth":22,"bounds":{"left":0.7081117,"top":1.0,"width":0.0023271276,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.71708775,"top":1.0,"width":0.011968086,"height":-0.06264961},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.7184175,"top":1.0,"width":0.043550532,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.73204786,"top":1.0,"width":0.011303191,"height":-0.063447714},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Galya Dimitrova","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-5895609627623219995
|
-141203165865289082
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
PnostormINavicatecodeFV faVsco.js?9 master k vProiect vc) ReindexForUserJob.oneketуAcuvilysyncJob.onosyncacuivity.ongleardownstream.onpD AiAutomation0 AiReportsD AudioAutomatedReportsC) RequestgenerateAskJiminnyReportJob.cC) RequestgenerateReportJob.pho© SendReportExpiringSoonMailJob.phpc) SendReportJob.php© SendReportMailJob.phpc) SendReoortNotGeneratedMail.loo.ond> M Calendarv 17Cmm› M Delete> M HubsootM Salesforce(c) Autoloabelavedtocrm.nhnl© CheckAndRetryRemoteMatch.php© CreateFollowupActivity.php© CreateNotes.php(@) MatchActivitiesToNewopportunity.ong(@) MatchActivitvermData.one(e NoteObiect.php© SaveActivity.php(e) SaveTranscriotion.onp© SetupLayout.phg© SyncActivity.php© SyncFieldMetadata.php© SyncHubspotObjects.php© SyncLeads.php@ SyncObiects.phpc) suncepoortunities.ob.onv@ SvncOpportunitv.ohrC) SvncProfileMetadata.oho@ SvncTeamFields.Job.phpC) SvncTeamMetadata.oho© [EMAIL]) UndateStade.oholM DealRisksM MailboyN MiddlewardD StreamingM ToamN Tolonhontmlisor(e) ChanadSmail lnh nhnC) DeactivateUserJob.phpCActivityController.ongC BaseService.php© SoftPhoneManager.php(C) CoreUserRequest.onp© CoreUser.php© Activity/RingCentral/Service.phpcSyncactiviLy.ono xccrm/close/service.ohoclass Syncactivity extends Job 1mplements Shouldoueue108110112113E E118146147148149150151165* dchrows Actzvitu?rovzderzxceotzon*dchrows SoczalAccountloken.nva.dzxcentzonl* dthrows containerExceotioninterface* dthrows NotFoundExcentionInterfacelusageorivate function init!ActivityImportManager SactivityImportManager,ActivityProviderRegistry SactivityProviderRegistry,UserRepository SuserRepository,SentryClient $sentry.LoggerInterface Slogger,): void {...}private function run: ActivityImportResultsth1s->Loqger->1ntosyncactivity start', sthis->context*Sthis-›activityImportManager->start($this->import):SimportedRecords = Sthis->service->importData(sth1s->import->qetstartbateohnthas->imoort->oetznouateoisthas->userreposttorv->r1ndunesvur'1d' => sth1s->1moort->cetuserd00.Sthis->import->qetActivitvIdoreturn inew ActivitvimoortResultool->addImnortedcsimnortedRecords)private function complete(ActivityImportResult $result): void(...}private function failImport(Throwable Sexception): void{...}= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 Gt ›146 @t>150151 G>156usadeorivate const int No GROUP 10 = 999orivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...}public function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}• мon 10 May 1/-00-22AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.nhn t17ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6152•14UTE.Afo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55296
|
1914
|
44
|
2026-05-18T14:00:26.570437+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112826570_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-862678653268360387
|
3190910648828842710
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Firefox•FileEdit→ViewHistoryBookmarksProfilesCToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100%8• Mon 18 May 17:00:26=A05Galya Dimitrova,Nikolay Yankov*4Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:46:53...
|
55294
|
NULL
|
NULL
|
NULL
|
|
55297
|
1914
|
45
|
2026-05-18T14:00:29.552817+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112829552_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08722222,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5","depth":22,"bounds":{"left":0.9145833,"top":0.09888889,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.08833333,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"bounds":{"left":0.26145834,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"bounds":{"left":0.2892361,"top":0.31,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Galya Dimitrova","depth":13,"bounds":{"left":0.31979167,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.10729167,"top":0.48277777,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.68645835,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.71423614,"top":0.31,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.7447917,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.53229165,"top":0.48277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.15729167,"top":0.70111114,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.18506944,"top":0.6988889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false}]...
|
-4565666502396353060
|
-267937252653900270
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
Firefox••FileEdit→ViewHistoryBookmarksProfilesCToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100%8• Mon 18 May 17:00:29=A05Galya DimitrovaNikolay Yankov*4Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:46:56...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55298
|
1914
|
46
|
2026-05-18T14:00:41.626619+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112841626_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Galya Dimitrova
Nikolay Yankov
Nikolay Ivanov
Aneliya Angelova
Lukas Kovalik
Others might see more of your background. Click to view your full video.
5:00
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍
Audio settings
Turn on microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08722222,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5","depth":22,"bounds":{"left":0.9145833,"top":0.09888889,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.08833333,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.10729167,"top":0.48277777,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.53229165,"top":0.48277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.057291668,"top":0.87166667,"width":0.07395833,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"bounds":{"left":0.37395832,"top":0.87166667,"width":0.088541664,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.69131947,"top":0.87166667,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.9607639,"top":0.86722225,"width":0.019444445,"height":0.031111112},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5:00","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.023958333,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.07777778,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"[Platform] Refinement 🔍","depth":12,"bounds":{"left":0.11180556,"top":0.9111111,"width":0.12881945,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[Platform] Refinement 🔍","depth":15,"bounds":{"left":0.11180556,"top":0.9438889,"width":0.12881945,"height":0.023333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Share screen","depth":12,"bounds":{"left":0.45451388,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"bounds":{"left":0.49895832,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"bounds":{"left":0.5434028,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"bounds":{"left":0.58784723,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
587243055443002730
|
-3724263087805505488
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Galya Dimitrova
Nikolay Yankov
Nikolay Ivanov
Aneliya Angelova
Lukas Kovalik
Others might see more of your background. Click to view your full video.
5:00
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍
Audio settings
Turn on microphone
Video settings
Turn off camera
Share screen
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
FirefoxFileEditViewHistoryBookmarksProfiles•→CToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100%8• Mon 18 May 17:00:41=A05+Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:47:08...
|
55297
|
NULL
|
NULL
|
NULL
|
|
55299
|
1915
|
28
|
2026-05-18T14:00:41.799446+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112841799_m2.jpg...
|
iTerm2
|
NULL
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PnostormVIewINavicatecodeFV faVsco.js?9 master k v PnostormVIewINavicatecodeFV faVsco.js?9 master k vProiect vc) ReindexForUserJob.oneketуAcuvilysyncJob.onosyncacuivity.ongleardownstream.onpD AiAutomation0 AiReportsD AudioAutomatedReportsC) RequestgenerateAskJiminnyReportJob.cC) RequestgenerateReportJob.pho© SendReportExpiringSoonMailJob.phpc) SendReportJob.php© SendReportMailJob.phpc) SendReoortNotGeneratedMail.loo.ond> M Calendarv 17Cmm› M Delete> M HubsootM Salesforce(c) Autoloabelavedtocrm.nhnl© CheckAndRetryRemoteMatch.php© CreateFollowupActivity.php© CreateNotes.php(@) MatchActivitiesToNewopportunity.ong(@) MatchActivitvermData.one(e NoteObiect.php© SaveActivity.php(e) SaveTranscriotion.onp© SetupLayout.phg© SyncActivity.php© SyncFieldMetadata.php© SyncHubspotObjects.php© SyncLeads.php@ SyncObiects.phpc) suncepoortunities.ob.onv@ SvncOpportunitv.ohrC) SvncProfileMetadata.oho@ SvncTeamFields.Job.phpC) SvncTeamMetadata.oho© [EMAIL]) UndateStade.oholM DealRisksM MailboyN MiddlewardD StreamingM ToamN Tolonhontmlisor(e) ChanadSmail lnh nhnC) DeactivateUserJob.phpCActivityController.ongC BaseService.php© SoftPhoneManager.php(C) CoreUserRequest.onp© CoreUser.php© Activity/RingCentral/Service.phpcSyncactiviLy.ono xccrm/close/service.ohoclass Syncactivity extends Job 1mplements Shouldoueue108110112113E E118146147148149150151165* dchrows Actzvitu?rovzderzxceotzon*dchrows SoczalAccountloken.nva.dzxcentzonl* dthrows containerExceotioninterface* dthrows NotFoundExcentionInterfacelusageorivate function init!ActivityImportManager SactivityImportManager,ActivityProviderRegistry SactivityProviderRegistry,UserRepository SuserRepository,SentryClient $sentry.LoggerInterface Slogger,): void {...}private function run: ActivityImportResultsth1s->Loqger->1ntosyncactivity start', sthis->context*Sthis-›activityImportManager->start($this->import):SimportedRecords = Sthis->service->importData(sth1s->import->qetstartbateohnthas->imoort->oetznouateoisthas->userreposttorv->r1ndunesvur'1d' => sth1s->1moort->cetuserd00.Sthis->import->qetActivitvIdoreturn inew ActivitvimoortResultool->addImnortedcsimnortedRecords)private function complete(ActivityImportResult $result): void(...}private function failImport(Throwable Sexception): void{...}= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 Gt ›146 @t>150151 G>156usadeorivate const int No GROUP 10 = 999orivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...}public function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}• мon 10 May 1/-00.47AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.nhn t17ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6152•14UTE.Afo 4 spaces...
|
NULL
|
-7025855580717163503
|
NULL
|
click
|
ocr
|
NULL
|
PnostormVIewINavicatecodeFV faVsco.js?9 master k v PnostormVIewINavicatecodeFV faVsco.js?9 master k vProiect vc) ReindexForUserJob.oneketуAcuvilysyncJob.onosyncacuivity.ongleardownstream.onpD AiAutomation0 AiReportsD AudioAutomatedReportsC) RequestgenerateAskJiminnyReportJob.cC) RequestgenerateReportJob.pho© SendReportExpiringSoonMailJob.phpc) SendReportJob.php© SendReportMailJob.phpc) SendReoortNotGeneratedMail.loo.ond> M Calendarv 17Cmm› M Delete> M HubsootM Salesforce(c) Autoloabelavedtocrm.nhnl© CheckAndRetryRemoteMatch.php© CreateFollowupActivity.php© CreateNotes.php(@) MatchActivitiesToNewopportunity.ong(@) MatchActivitvermData.one(e NoteObiect.php© SaveActivity.php(e) SaveTranscriotion.onp© SetupLayout.phg© SyncActivity.php© SyncFieldMetadata.php© SyncHubspotObjects.php© SyncLeads.php@ SyncObiects.phpc) suncepoortunities.ob.onv@ SvncOpportunitv.ohrC) SvncProfileMetadata.oho@ SvncTeamFields.Job.phpC) SvncTeamMetadata.oho© [EMAIL]) UndateStade.oholM DealRisksM MailboyN MiddlewardD StreamingM ToamN Tolonhontmlisor(e) ChanadSmail lnh nhnC) DeactivateUserJob.phpCActivityController.ongC BaseService.php© SoftPhoneManager.php(C) CoreUserRequest.onp© CoreUser.php© Activity/RingCentral/Service.phpcSyncactiviLy.ono xccrm/close/service.ohoclass Syncactivity extends Job 1mplements Shouldoueue108110112113E E118146147148149150151165* dchrows Actzvitu?rovzderzxceotzon*dchrows SoczalAccountloken.nva.dzxcentzonl* dthrows containerExceotioninterface* dthrows NotFoundExcentionInterfacelusageorivate function init!ActivityImportManager SactivityImportManager,ActivityProviderRegistry SactivityProviderRegistry,UserRepository SuserRepository,SentryClient $sentry.LoggerInterface Slogger,): void {...}private function run: ActivityImportResultsth1s->Loqger->1ntosyncactivity start', sthis->context*Sthis-›activityImportManager->start($this->import):SimportedRecords = Sthis->service->importData(sth1s->import->qetstartbateohnthas->imoort->oetznouateoisthas->userreposttorv->r1ndunesvur'1d' => sth1s->1moort->cetuserd00.Sthis->import->qetActivitvIdoreturn inew ActivitvimoortResultool->addImnortedcsimnortedRecords)private function complete(ActivityImportResult $result): void(...}private function failImport(Throwable Sexception): void{...}= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 Gt ›146 @t>150151 G>156usadeorivate const int No GROUP 10 = 999orivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...}public function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}• мon 10 May 1/-00.47AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.nhn t17ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6152•14UTE.Afo 4 spaces...
|
55295
|
NULL
|
NULL
|
NULL
|
|
55300
|
1914
|
47
|
2026-05-18T14:00:44.608270+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112844608_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditView→HistoryBookmarksProfilesToolsW FirefoxFileEditView→HistoryBookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% <78 • Mon 18 May 17:00:44A05000Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:47:11...
|
NULL
|
-3131327756003411310
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditView→HistoryBookmarksProfilesToolsW FirefoxFileEditView→HistoryBookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% <78 • Mon 18 May 17:00:44A05000Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:47:11...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55301
|
1914
|
48
|
2026-05-18T14:00:47.667776+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112847667_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
-4047408223668449518
|
-8636777727364969024
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
Firefox• 0FileEdit→ViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kova100% C8 • Mon 18 May 17:00:4740jiminny.com5Galya DimitrovNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement ®1:47:14...
|
55300
|
NULL
|
NULL
|
NULL
|
|
55302
|
1914
|
49
|
2026-05-18T14:00:50.721504+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112850721_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
3303896123807886662
|
-1423066649041924143
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55303
|
1914
|
50
|
2026-05-18T14:00:53.752370+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112853752_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"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}]...
|
-4411325496289755485
|
-1414059449787118639
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification...
|
55302
|
NULL
|
NULL
|
NULL
|
|
55304
|
1914
|
51
|
2026-05-18T14:00:56.789284+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112856789_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
8243381250999052583
|
-8204424741936591934
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
Firefox• 0FileEditViewHistoryBookmarksProfiles→ToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% <78 • Mon 18 May 17:00:56)A05Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:00 PM | [Platform] Refinement'1:47:23...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55305
|
1914
|
52
|
2026-05-18T14:00:59.818452+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112859818_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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}]...
|
6088908589105147991
|
-1414059449518157871
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide...
|
55304
|
NULL
|
NULL
|
NULL
|
|
55306
|
1914
|
53
|
2026-05-18T14:01:02.823546+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112862823_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"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":"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\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\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}]...
|
6765114288521301901
|
-259430402405920803
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55307
|
1914
|
54
|
2026-05-18T14:01:05.840366+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112865840_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"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}]...
|
7870932135135850543
|
-1414059449786593327
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes...
|
55306
|
NULL
|
NULL
|
NULL
|
|
55308
|
1914
|
55
|
2026-05-18T14:01:11.877853+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112871877_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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}]...
|
6088908589105147991
|
-1414059449518157871
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55309
|
1915
|
29
|
2026-05-18T14:01:16.052890+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112876052_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.6449468,"top":0.92098963,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.6449468,"top":0.952913,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"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":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
3303896123807886662
|
-1423066649041924143
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55310
|
1914
|
56
|
2026-05-18T14:01:17.917110+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112877917_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% <78 • Mon 18 May 17:01:175Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:01 PM | [Platform] Refinement®1:47:44...
|
NULL
|
813183072280314651
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditViewHistory→BookmarksProfilesToolsW FirefoxFileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% <78 • Mon 18 May 17:01:175Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:01 PM | [Platform] Refinement®1:47:44...
|
55308
|
NULL
|
NULL
|
NULL
|
|
55311
|
1914
|
57
|
2026-05-18T14:01:20.919063+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112880919_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"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":"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}]...
|
5930764665635139907
|
-1414041857601057839
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55312
|
1914
|
58
|
2026-05-18T14:01:26.970927+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112886970_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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}]...
|
6088908589105147991
|
-1414059449518157871
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide...
|
55311
|
NULL
|
NULL
|
NULL
|
|
55313
|
1914
|
59
|
2026-05-18T14:01:30.003174+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112890003_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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}]...
|
-7429716278976468786
|
-8636355650190325311
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
FirefoxFileEdit→ViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% C8 • Mon 18 May 17:01:29=A05••Galya DimitrovaNikolay Yankov*Nikolay IvanovAneliya AngelovaLukas Kovalik5:01 PM | [Platform] Refinement®1:47:56...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
55314
|
1914
|
60
|
2026-05-18T14:01:36.052883+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112896052_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"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":"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\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-819186497728337901
|
-259430539844874275
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}...
|
55313
|
NULL
|
NULL
|
NULL
|
|
55315
|
1914
|
61
|
2026-05-18T14:01:39.084314+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779112899084_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.7826389,"top":0.0,"width":0.14513889,"height":0.015555556},"on_screen":true,"role_description":"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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","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":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Activity;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Component\\Sentry\\SentryClient;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Contracts\\Services\\ImportsDataInterface;\nuse Jiminny\\DTO\\Activity\\Import\\ActivityImportResult;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Activity\\ActivityImport;\nuse Jiminny\\Models\\Activity\\Provider;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Import\\ActivityImportManager;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\nclass SyncActivity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';\n\n public int $tries = 3;\n public $queue = Constants::QUEUE_DIALERS;\n\n private ActivityImport $import;\n private ?ActivityImportManager $activityImportManager = null;\n private ?UserRepository $userRepository = null;\n private ?SentryClient $sentryClient = null;\n private ?LoggerInterface $logger = null;\n private ?Provider $provider = null;\n private ?ImportsDataInterface $service = null;\n private array $context;\n\n public function __construct(ActivityImport $import)\n {\n $this->import = $import;\n $this->context = ['import_id' => $import->getId()];\n }\n\n public function handle(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n try {\n $this->init(\n $activityImportManager,\n $activityProviderRegistry,\n $userRepository,\n $sentry,\n $logger,\n );\n } catch (Throwable $exception) {\n $this->failImport($exception);\n\n return;\n }\n\n Datadog::increment('jiminny.activity.sync.start', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n try {\n $result = $this->run();\n $this->complete($result);\n } catch (Throwable $exception) {\n $this->failImport($exception);\n }\n }\n\n public function failed(Throwable $exception): void\n {\n $this->logger ??= app(LoggerInterface::class);\n $this->activityImportManager ??= app(ActivityImportManager::class);\n $this->sentryClient ??= app(SentryClient::class);\n\n if (! isset($this->context)) {\n // This can happen in case of improper deserialization.\n $this->context = [];\n }\n $this->context['team'] ??= 'unknown';\n $this->context['provider'] ??= 'unknown';\n\n if (isset($this->import)) {\n // This can happen in case of improper deserialization.\n $this->failImport($exception);\n }\n }\n\n /**\n * @throws ActivityProviderException\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function init(\n ActivityImportManager $activityImportManager,\n ActivityProviderRegistry $activityProviderRegistry,\n UserRepository $userRepository,\n SentryClient $sentry,\n LoggerInterface $logger,\n ): void {\n $this->logger = $logger;\n $this->activityImportManager = $activityImportManager;\n $this->userRepository = $userRepository;\n $this->sentryClient = $sentry;\n $provider = $this->import->getActivityProvider();\n\n $this->context['provider'] = $provider->getProviderSlug();\n $this->context['provider_id'] = $provider->getId();\n $this->context['team'] = $provider->getTeam()->getSlug();\n $this->context['team_id'] = $provider->getTeamId();\n\n if (! $provider->isEnabled()) {\n throw new ActivityProviderException('Activity provider is disabled', $provider);\n }\n\n $this->provider = $provider;\n $service = $activityProviderRegistry->get($provider->getProviderSlug());\n\n if (! $service instanceof ImportsDataInterface) {\n throw new ActivityProviderException('Activity provider does not support data imports', $provider);\n }\n\n $service->setTeam($provider->getTeam());\n $service->setSocialAccount($provider->getConnectedSocialAccount());\n $service->setDebug($this->import->runsInDebug());\n $this->service = $service;\n }\n\n private function run(): ActivityImportResult\n {\n $this->logger->info('[SyncActivity] Start', $this->context);\n $this->activityImportManager->start($this->import);\n\n $importedRecords = $this->service->importData(\n $this->import->getStartDate(),\n $this->import->getEndDate(),\n $this->userRepository->findOneBy(['id' => $this->import->getUserId()]),\n $this->import->getActivityId()\n );\n\n return (new ActivityImportResult())\n ->setTotal($importedRecords)\n ->addImported($importedRecords)\n ;\n }\n\n private function complete(ActivityImportResult $result): void\n {\n $this->activityImportManager->complete($this->import, $result);\n\n Datadog::increment('jiminny.activity.sync.success', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n ]);\n\n $this->logger->info('[SyncActivity] End', $this->context);\n\n $this->logger->info(\n '[SyncActivity] Memory usage',\n array_merge(\n $this->context,\n [\n 'memory_usage' => memory_get_usage(),\n 'memory_real_usage' => memory_get_usage(true),\n 'pid' => getmypid(),\n ],\n ),\n );\n }\n\n private function failImport(Throwable $exception): void\n {\n $this->logger->alert(\n message: '[SyncActivity] Failed',\n context: array_merge($this->context, [\n 'reason' => $exception->getMessage(),\n 'file' => $exception->getFile(),\n 'line' => $exception->getLine(),\n ]),\n );\n\n $this->activityImportManager->fail($this->import);\n\n Datadog::increment('jiminny.activity.sync.exception', 1.0, [\n 'company' => $this->context['team'],\n 'provider' => $this->context['provider'],\n 'exception' => $exception::class,\n 'exceptionCode' => $exception->getCode(),\n ]);\n\n if ($exception instanceof SocialAccountTokenInvalidException) {\n return;\n }\n\n $this->sentryClient->captureException($exception);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"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":"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\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\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}]...
|
8729639483820449576
|
-259430402414309411
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, 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
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Activity;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Jiminny\Component\Queue\Constants;
use Jiminny\Component\Sentry\SentryClient;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Contracts\Services\ImportsDataInterface;
use Jiminny\DTO\Activity\Import\ActivityImportResult;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Activity\ActivityImport;
use Jiminny\Models\Activity\Provider;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Import\ActivityImportManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class SyncActivity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
public const string ALLOWED_DATE_FORMAT = 'Y-m-d H:i:s';
public int $tries = 3;
public $queue = Constants::QUEUE_DIALERS;
private ActivityImport $import;
private ?ActivityImportManager $activityImportManager = null;
private ?UserRepository $userRepository = null;
private ?SentryClient $sentryClient = null;
private ?LoggerInterface $logger = null;
private ?Provider $provider = null;
private ?ImportsDataInterface $service = null;
private array $context;
public function __construct(ActivityImport $import)
{
$this->import = $import;
$this->context = ['import_id' => $import->getId()];
}
public function handle(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
try {
$this->init(
$activityImportManager,
$activityProviderRegistry,
$userRepository,
$sentry,
$logger,
);
} catch (Throwable $exception) {
$this->failImport($exception);
return;
}
Datadog::increment('jiminny.activity.sync.start', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
try {
$result = $this->run();
$this->complete($result);
} catch (Throwable $exception) {
$this->failImport($exception);
}
}
public function failed(Throwable $exception): void
{
$this->logger ??= app(LoggerInterface::class);
$this->activityImportManager ??= app(ActivityImportManager::class);
$this->sentryClient ??= app(SentryClient::class);
if (! isset($this->context)) {
// This can happen in case of improper deserialization.
$this->context = [];
}
$this->context['team'] ??= 'unknown';
$this->context['provider'] ??= 'unknown';
if (isset($this->import)) {
// This can happen in case of improper deserialization.
$this->failImport($exception);
}
}
/**
* @throws ActivityProviderException
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function init(
ActivityImportManager $activityImportManager,
ActivityProviderRegistry $activityProviderRegistry,
UserRepository $userRepository,
SentryClient $sentry,
LoggerInterface $logger,
): void {
$this->logger = $logger;
$this->activityImportManager = $activityImportManager;
$this->userRepository = $userRepository;
$this->sentryClient = $sentry;
$provider = $this->import->getActivityProvider();
$this->context['provider'] = $provider->getProviderSlug();
$this->context['provider_id'] = $provider->getId();
$this->context['team'] = $provider->getTeam()->getSlug();
$this->context['team_id'] = $provider->getTeamId();
if (! $provider->isEnabled()) {
throw new ActivityProviderException('Activity provider is disabled', $provider);
}
$this->provider = $provider;
$service = $activityProviderRegistry->get($provider->getProviderSlug());
if (! $service instanceof ImportsDataInterface) {
throw new ActivityProviderException('Activity provider does not support data imports', $provider);
}
$service->setTeam($provider->getTeam());
$service->setSocialAccount($provider->getConnectedSocialAccount());
$service->setDebug($this->import->runsInDebug());
$this->service = $service;
}
private function run(): ActivityImportResult
{
$this->logger->info('[SyncActivity] Start', $this->context);
$this->activityImportManager->start($this->import);
$importedRecords = $this->service->importData(
$this->import->getStartDate(),
$this->import->getEndDate(),
$this->userRepository->findOneBy(['id' => $this->import->getUserId()]),
$this->import->getActivityId()
);
return (new ActivityImportResult())
->setTotal($importedRecords)
->addImported($importedRecords)
;
}
private function complete(ActivityImportResult $result): void
{
$this->activityImportManager->complete($this->import, $result);
Datadog::increment('jiminny.activity.sync.success', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
]);
$this->logger->info('[SyncActivity] End', $this->context);
$this->logger->info(
'[SyncActivity] Memory usage',
array_merge(
$this->context,
[
'memory_usage' => memory_get_usage(),
'memory_real_usage' => memory_get_usage(true),
'pid' => getmypid(),
],
),
);
}
private function failImport(Throwable $exception): void
{
$this->logger->alert(
message: '[SyncActivity] Failed',
context: array_merge($this->context, [
'reason' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]),
);
$this->activityImportManager->fail($this->import);
Datadog::increment('jiminny.activity.sync.exception', 1.0, [
'company' => $this->context['team'],
'provider' => $this->context['provider'],
'exception' => $exception::class,
'exceptionCode' => $exception->getCode(),
]);
if ($exception instanceof SocialAccountTokenInvalidException) {
return;
}
$this->sentryClient->captureException($exception);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|