|
56153
|
1953
|
16
|
2026-05-19T07:31:33.623262+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175893623_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56150
|
NULL
|
NULL
|
NULL
|
|
56154
|
1952
|
12
|
2026-05-19T07:31:35.265149+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175895265_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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
|
|
56155
|
1952
|
13
|
2026-05-19T07:31:59.676176+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175919676_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowH iTerm2ShellEditViewSessionScriptsProfilesWindowHelp• Support Daily • in 4 h 29 m100% C78• Tue 19 May 10:31:59-zshAPP (-zsh)DOCKERO ₴1DEV (-zsh)₴82*3screenpipe"0 ₴4-zsh=>[api internal] load builddefinitionfromDockerfile= => transferring dockerfile: 567B=> [api] resolve image [URL_WITH_CREDENTIALS] [api internal] load build definitionfrom Dockerfile=> WARN: JSONArgsRecommended:JSONarguments recommended for CMD to prevent unintended behavior related to OS signals (line 21)=> [mcp internal] load metadata for docker.io/library/python:3.11-slim=> [api internal] loaddockerignore= => transferring context: 2B= [api 1/7] FROM docker.io/library/python:3.11-slim= [api internal] load build context= = transferring context: 60.33kB=> CACHED [api 2/7] WORKDIR/app=> CACHED [api 3/7] COPY requirements.txt /app/=> CACHED [api 4/7] RUN pip install --no-cache-dir -r requirements.txt=> [api 5/7] COPY app /app/app|=> [api 6/7] COPY alembic/app/alembic[api 7/7] COPY alembic.ini /app/alembic.ini=> [api] exporting to image= => exporting layers= => writing image sha256:0b6f06ab29cc13dc1256d9e8240bc4bbd7ab34630040c12aae54547fb10233ec= => namingto docker.io/library/location-logger-api=> [mcp internal] load build definition from Dockerfile= => transferring dockerfile: 715B[mcp internal] loaddockerignore=> transferring context: 2B[mcp internal] load build context= transferring context: 115B[mcр 1/6]FROM docker.io/library/python:3.11-slim=>CACHED [mcp 2/6] WORKDIR /appCACHED [mcp 3/6] COPY requirements.txt /app/CACHED[mср4/6]RUN pip install--no-cache-dir -r requirements.txt=> CACHED[mcp5/6J RUNSITE=$(python -c"import sysconfig; print(sysconfig.get_path('purelib'))")=> CACHED [mcр6/6] COPYserver.py /app/&& sed-i's/enable_dns_rebinding_protection=True/enable_dns_rebindin=> [mcp] exporting to image=>=> exportinglayers= => writingimage sha256:afd9cc01d29616aa089d8ca3b164aaec06a200e88872d3ad4e2a87432aa68bc0=> =› naming to docker.io/library/location-logger-mcp[+] Running 3/3• Container location-logger-postgresHealthy• Container location-logger-apiHealthy• Container location-logger-mcpStartedAdm1n@DXP4800PLUS-B5F8:/volume2/docker/location-logger$ Connection to [IP_ADDRESS] closed by remote host.Connection to [IP_ADDRESS] closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ |*50.0s0.050.950.0s0.0s0.0s0.050.050.0s0.0s0.150.050.050.0s0.0s0.250.250.2s0.250.2s0.0s0.050.050.0s0.0s0.0s0.050.0s0.050.050.0s0.050.0s0.0s0.0s0.0s0.050.0s0.0510.6s0.85...
|
NULL
|
-4227764661910611277
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowH iTerm2ShellEditViewSessionScriptsProfilesWindowHelp• Support Daily • in 4 h 29 m100% C78• Tue 19 May 10:31:59-zshAPP (-zsh)DOCKERO ₴1DEV (-zsh)₴82*3screenpipe"0 ₴4-zsh=>[api internal] load builddefinitionfromDockerfile= => transferring dockerfile: 567B=> [api] resolve image [URL_WITH_CREDENTIALS] [api internal] load build definitionfrom Dockerfile=> WARN: JSONArgsRecommended:JSONarguments recommended for CMD to prevent unintended behavior related to OS signals (line 21)=> [mcp internal] load metadata for docker.io/library/python:3.11-slim=> [api internal] loaddockerignore= => transferring context: 2B= [api 1/7] FROM docker.io/library/python:3.11-slim= [api internal] load build context= = transferring context: 60.33kB=> CACHED [api 2/7] WORKDIR/app=> CACHED [api 3/7] COPY requirements.txt /app/=> CACHED [api 4/7] RUN pip install --no-cache-dir -r requirements.txt=> [api 5/7] COPY app /app/app|=> [api 6/7] COPY alembic/app/alembic[api 7/7] COPY alembic.ini /app/alembic.ini=> [api] exporting to image= => exporting layers= => writing image sha256:0b6f06ab29cc13dc1256d9e8240bc4bbd7ab34630040c12aae54547fb10233ec= => namingto docker.io/library/location-logger-api=> [mcp internal] load build definition from Dockerfile= => transferring dockerfile: 715B[mcp internal] loaddockerignore=> transferring context: 2B[mcp internal] load build context= transferring context: 115B[mcр 1/6]FROM docker.io/library/python:3.11-slim=>CACHED [mcp 2/6] WORKDIR /appCACHED [mcp 3/6] COPY requirements.txt /app/CACHED[mср4/6]RUN pip install--no-cache-dir -r requirements.txt=> CACHED[mcp5/6J RUNSITE=$(python -c"import sysconfig; print(sysconfig.get_path('purelib'))")=> CACHED [mcр6/6] COPYserver.py /app/&& sed-i's/enable_dns_rebinding_protection=True/enable_dns_rebindin=> [mcp] exporting to image=>=> exportinglayers= => writingimage sha256:afd9cc01d29616aa089d8ca3b164aaec06a200e88872d3ad4e2a87432aa68bc0=> =› naming to docker.io/library/location-logger-mcp[+] Running 3/3• Container location-logger-postgresHealthy• Container location-logger-apiHealthy• Container location-logger-mcpStartedAdm1n@DXP4800PLUS-B5F8:/volume2/docker/location-logger$ Connection to [IP_ADDRESS] closed by remote host.Connection to [IP_ADDRESS] closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ |*50.0s0.050.950.0s0.0s0.0s0.050.050.0s0.0s0.150.050.050.0s0.0s0.250.250.2s0.250.2s0.0s0.050.050.0s0.0s0.0s0.050.0s0.050.050.0s0.050.0s0.0s0.0s0.0s0.050.0s0.0510.6s0.85...
|
56154
|
NULL
|
NULL
|
NULL
|
|
56156
|
1953
|
17
|
2026-05-19T07:31:59.682189+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175919682_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicareCodeLaravelKeractorFV faVsco. PhostormVIewINavicareCodeLaravelKeractorFV faVsco.js~°9 master kProject© SoftPhoneManager.php© CoreUserRequest.php© LiveCoachController.php© MissingTeamController.phpc) Mobilecontroller.ong© NotificationController.phpONowricationrrovidercontroller.onp© PlaybackConttroller.php© PlaylistController.php© PusherController.phpSlackController.php© SupportController.php© TeamSetupController.phpc) Userautomareakeporiscontroller.pnpc) welcomecontroller.onoconstants.ongActivity/Close/service.pnp© Activity/RingCentral/Service.phpsyncactivily.phpclass SyncActivity extends Job implements ShouldQueueorivateActivirvimoortResultsch1s->1mport->gectnovate,$this-›userRepository->findOneBy(['id' => $this->import->getUserIdO]),sch1s->1mport->gecacclv1cy10MicclewareC RequestsSerializersM Transformers(C) Kernelohd©PlaylistTrackResourceTrait.phpT ValidateCrmConnectionReguiredTrait.ohoIntegrationsm InteractionsD JobsD Activity>D DialpadM imnort.O JustCallPushSummaryToCrmRingCentralm 7oomDhanc© ActivityChangeCategorylds.phpAssignownersnip.onp© ConferenceCrmMatcherJob.php© DeleteActivities.php© DeleteTeamChurnData.php© DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpC)MatchMeetngowner.onv© ReindexForAccountJob.phpC) ReindexForContact.Job.ohvC) ReindexForGrouo.Job.ohoC) ReindexForLead.Job.ohnC) [EMAIL] (new ActivityImportresulto)->settotal(SimportedRecords).->addImported($importedRecords)usayeprivate function complete(ActivityImportResult $result): voidSthis->activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog:: increment( stats: "jiminny.activity.sync.success','company' => $this->context['team'],'provider' => $this->context['provider'],sampleRate: 1.0, [D);$this->logger->info('[SyncActivity] End', $this->context);$this->logger->info('[SyncActivity] Memory usage',array_merged'memory usage => memory get usageo'memory real usage' => memory qet usagec real_ usage: true).'pid' => getmypid(),orivate function faluumoortuhrowable Sexcention): void ..215C) ReindexForUser.Job.oho(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nhn(C) TeardownStream nhnM AiAutomation# Support Daily - in 4h 29 mU AskJiminnyReportActivityServiceTest~100% C4&• Tue 19 May 10:31:59+0 ..Ecustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]& console [EU]CascadeCascadeA1. Ydeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 € >146 @ >150151 G>1usadeorivate 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ßBeIncluded(Team $team): boolf...}wCascade Code *•Kick off a new project. Make changesacross your entre codedase• SCIM Role Management Implementationc Salesforce Token Fallback© Fixing Redis Rate Limit ErrorAsk anvthina (884-L)WN Windsurf Toams 178-25UTF.8f?4 spaces...
|
NULL
|
-3365229625752749606
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicareCodeLaravelKeractorFV faVsco. PhostormVIewINavicareCodeLaravelKeractorFV faVsco.js~°9 master kProject© SoftPhoneManager.php© CoreUserRequest.php© LiveCoachController.php© MissingTeamController.phpc) Mobilecontroller.ong© NotificationController.phpONowricationrrovidercontroller.onp© PlaybackConttroller.php© PlaylistController.php© PusherController.phpSlackController.php© SupportController.php© TeamSetupController.phpc) Userautomareakeporiscontroller.pnpc) welcomecontroller.onoconstants.ongActivity/Close/service.pnp© Activity/RingCentral/Service.phpsyncactivily.phpclass SyncActivity extends Job implements ShouldQueueorivateActivirvimoortResultsch1s->1mport->gectnovate,$this-›userRepository->findOneBy(['id' => $this->import->getUserIdO]),sch1s->1mport->gecacclv1cy10MicclewareC RequestsSerializersM Transformers(C) Kernelohd©PlaylistTrackResourceTrait.phpT ValidateCrmConnectionReguiredTrait.ohoIntegrationsm InteractionsD JobsD Activity>D DialpadM imnort.O JustCallPushSummaryToCrmRingCentralm 7oomDhanc© ActivityChangeCategorylds.phpAssignownersnip.onp© ConferenceCrmMatcherJob.php© DeleteActivities.php© DeleteTeamChurnData.php© DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpC)MatchMeetngowner.onv© ReindexForAccountJob.phpC) ReindexForContact.Job.ohvC) ReindexForGrouo.Job.ohoC) ReindexForLead.Job.ohnC) [EMAIL] (new ActivityImportresulto)->settotal(SimportedRecords).->addImported($importedRecords)usayeprivate function complete(ActivityImportResult $result): voidSthis->activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog:: increment( stats: "jiminny.activity.sync.success','company' => $this->context['team'],'provider' => $this->context['provider'],sampleRate: 1.0, [D);$this->logger->info('[SyncActivity] End', $this->context);$this->logger->info('[SyncActivity] Memory usage',array_merged'memory usage => memory get usageo'memory real usage' => memory qet usagec real_ usage: true).'pid' => getmypid(),orivate function faluumoortuhrowable Sexcention): void ..215C) ReindexForUser.Job.oho(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nhn(C) TeardownStream nhnM AiAutomation# Support Daily - in 4h 29 mU AskJiminnyReportActivityServiceTest~100% C4&• Tue 19 May 10:31:59+0 ..Ecustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]& console [EU]CascadeCascadeA1. Ydeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 € >146 @ >150151 G>1usadeorivate 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ßBeIncluded(Team $team): boolf...}wCascade Code *•Kick off a new project. Make changesacross your entre codedase• SCIM Role Management Implementationc Salesforce Token Fallback© Fixing Redis Rate Limit ErrorAsk anvthina (884-L)WN Windsurf Toams 178-25UTF.8f?4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56157
|
NULL
|
0
|
2026-05-19T07:32:30.389001+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175950389_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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
|
|
56158
|
NULL
|
0
|
2026-05-19T07:32:30.850834+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175950850_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56156
|
NULL
|
NULL
|
NULL
|
|
56159
|
1954
|
0
|
2026-05-19T07:33:00.611882+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175980611_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56160
|
1955
|
0
|
2026-05-19T07:33:01.113937+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779175981113_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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
|
|
56161
|
1954
|
1
|
2026-05-19T07:33:30.867337+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176010867_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56162
|
1955
|
1
|
2026-05-19T07:33:31.385861+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176011385_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56160
|
NULL
|
NULL
|
NULL
|
|
56163
|
1954
|
2
|
2026-05-19T07:34:01.083191+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176041083_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56164
|
1955
|
2
|
2026-05-19T07:34:01.643245+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176041643_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56160
|
NULL
|
NULL
|
NULL
|
|
56165
|
1954
|
3
|
2026-05-19T07:34:31.288853+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176071288_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56166
|
1955
|
3
|
2026-05-19T07:34:31.905546+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176071905_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56160
|
NULL
|
NULL
|
NULL
|
|
56167
|
1954
|
4
|
2026-05-19T07:35:01.524308+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176101524_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56168
|
1955
|
4
|
2026-05-19T07:35:02.201516+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176102201_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56160
|
NULL
|
NULL
|
NULL
|
|
56169
|
1954
|
5
|
2026-05-19T07:35:31.733802+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176131733_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56170
|
1955
|
5
|
2026-05-19T07:35:32.467568+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176132467_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56160
|
NULL
|
NULL
|
NULL
|
|
56172
|
1955
|
7
|
2026-05-19T07:35:57.120899+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176157120_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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
|
|
56173
|
1954
|
6
|
2026-05-19T07:36:01.930796+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176161930_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56174
|
1955
|
8
|
2026-05-19T07:36:27.397013+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176187397_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56172
|
NULL
|
NULL
|
NULL
|
|
56175
|
1954
|
7
|
2026-05-19T07:36:32.140467+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176192140_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56176
|
1955
|
9
|
2026-05-19T07:36:57.662124+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176217662_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56172
|
NULL
|
NULL
|
NULL
|
|
56177
|
1954
|
8
|
2026-05-19T07:37:02.366345+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176222366_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56178
|
1955
|
10
|
2026-05-19T07:37:27.933070+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176247933_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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":"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.08843085,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.09940159,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"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.10804521,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.11668883,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.12533244,"top":0.05027933,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56172
|
NULL
|
NULL
|
NULL
|
|
56179
|
1954
|
9
|
2026-05-19T07:37:32.560039+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176252560_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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},{"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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56180
|
NULL
|
0
|
2026-05-19T07:37:36.371365+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176256371_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"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}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
iTerm2Shel Project: faVsco.js, menu
master, menu
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp• Support Daily - in 4 h 23 m100% C78• Tue 19 May 10:37:35-zshDOCKERO $1DEV (-zsh)₴82APP (-zsh)*3screenpipe"₴84-zsh*5=>[api internal] load builddefinitionfromDockerfile0.0s= => transferring dockerfile: 567B0.05=> [api] resolve image [URL_WITH_CREDENTIALS] [api internal] load build definitionfrom Dockerfile0.0s=> WARN: JSONArgsRecommended:JSONarguments recommended for CMD to prevent unintended behavior related to OS signals (line 21)0.0s=> [mcp internal] load metadata fordocker.io/library/python:3.11-slim0.05=> [api internal] loaddockerignore0.05= => transferring context: 2B0.0s= [api 1/7] FROM docker.io/library/python:3.11-slim0.0s= [api internal] load build context0.15= = transferring context: 60.33kB0.05=> CACHED [api 2/7] WORKDIR/app0.05=> CACHED [api 3/7] COPY requirements.txt /app/0.0s=> CACHED [api 4/7] RUNpip install --no-cache-dir -r requirements.txt0.0s=> [api 5/7] COPY app /app/app|0.25=> [api 6/7] COPY alembic/app/alembic0.25[api 7/7] COPY alembic.ini /app/alembic.ini0.2s=> [api] exporting to image0.25= => exporting layers0.2s= => writing image sha256:0b6f06ab29cc13dc1256d9e8240bc4bbd7ab34630040c12aae54547fb10233ec0.0s= = namingto docker.io/library/location-logger-api0.05=> [mcp internal] load build definition from Dockerfile0.05= => transferring dockerfile: 715B0.0s[mcp internal] loaddockerignore0.0s=> transferring context: 2B0.0s[mcp internal] load build context0.05= transferring context: 115B0.0s[mcр 1/6]FROM docker.io/library/python:3.11-slim0.05=>CACHED [mcp 2/6] WORKDIR /app0.05CACHED [mcp 3/6] COPY requirements.txt /app/0.0sCACHED[mср4/6]RUN pip install--no-cache-dir -r requirements.txt0.05=> CACHED[mcp5/6J RUNSITE=$(python -c"import sysconfig; print(sysconfig.get_path('purelib'))")&& sed-i=> CACHED [mcр6/6J COPYserver.py /app/'s/enable_dns_rebinding_protection=True/enable_dns_rebindin0.0s0.0s=> [mcp] exporting to image0.0s=>=> exportinglayers0.0s= => writingimage sha256:afd9cc01d29616aa089d8ca3b164aaec06a200e88872d3ad4e2a87432aa68bc00.05=> =› naming to docker.io/library/location-logger-mcp0.0s[+] Running 3/3• Container location-logger-postgresHealthy• Container location-logger-apiHealthy• Container location-logger-mcpStarted0.0510.6s0.85Adm1n@DXP4800PLUS-B5F8:/volume2/docker/location-logger$ Connection to [IP_ADDRESS] closed by remote host.Connection to [IP_ADDRESS] closed.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ [...
|
56157
|
NULL
|
NULL
|
NULL
|
|
56181
|
NULL
|
0
|
2026-05-19T07:37:36.381993+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176256381_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicareCodeLaravelFV faVsco.js~°9 ma PhostormVIewINavicareCodeLaravelFV faVsco.js~°9 master kProject© LiveCoachController.php© MissingTeamController.phpc) Mobilecontroller.ong© NotificationController.phpONowricationrrovidercontroller.onp© PlaybackConttroller.php© PlaylistController.php© PusherController.php© SlackController.php© SupportController.php© TeamSetupController.phpc) Userautomateakeporiscontroller.pnoc) welcomecontroller.onoU MicclewareSerializersM Transformers(C) Kernelohr©PlaylistTrackResourceTrait.phpT ValidateCrmConnectionReguiredtrait.oho• IntegrationsInteractionsJobsv Activity> Dialpad>D ImportO JustCallPushSummaryToCrm• D RingCentralm 7oomDhanc© ActivityChangeCategorylds.phpAssicnownersnip.onp© ConferenceCrmMatcherJob.phpC) DeleteActivities.php© DeleteTeamChurnData.phpC) DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpC)MatchMeetngowner.onvc) ReindexForAccount.o..ohoC) ReindexForContact.Job.ohvC) ReindexForGrouo.Job.ohoC) ReindexForLead.Job.ohnC) [EMAIL]) ReindexForUser.Job.oho(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nho(C) TeardownStream nhnM AiAutomationKeractor© BaseService.php© SoftPhoneManager.php© CoreUserRequest.phpconstants.ongy coreuser.pnpActivity/Close/service.pnp© Activity/RingCentral/Service.phpsyncacuivity.php165166167168215class SyncActivity extends Job implements ShouldQueueorivatetunction runo.ActivirvimoortResultsch1s->1mport->gectnovate,$this-›userRepository->findOneBy(['id' => $this->import->getUserIdO]),sch1s->1mport->gecAcc1V1cy100return new ActivitvimportResultoo->settotal SimportedRecords).->addImported($importedRecords)usayeprivate function complete(ActivityImportResult $result): voidSthis->activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog:: increment( stats: "jiminny.activity.sync.success','company' => $this->context['team'],'provider' => $this->context['provider'],sampleRate: 1.0, [D);$this->logger->info('[SyncActivity] End', $this->context);$this->logger->info("Lsyncaccivity. renory usage"arraymeroeu'memory usage => memory get usageo.'memoryreal usage' => memory qet usagec real usage: true)'pid' => getmypid(),orivate function faluumoortuhrowable Sexcention: void .?The Hunsnell nluain hac heon denrecated. If vou're not writing in Hungarian vou canEcustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]& console [EU]A1. Ydeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 € >146 @ >150151 G>1usadeorivate 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ßBeIncluded(Team $team): boolf...}CascadeCascadef Support Daily - in 4h 23mU AskJiminnyReportActivityServiceTest~100% Lz&• Tue 19 May 10:37:36+0 ..wCascade Code *•Kick off a new project. Make changesacross your entre codedase• SCIM Role Management Implementationc Salesforce Token Fallback© Fixing Redis Rate Limit ErrorAsk anvthina (884-L)WN Windsurf Toams 178-25UTF.8f?4 spaces...
|
NULL
|
-956710474745480910
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicareCodeLaravelFV faVsco.js~°9 ma PhostormVIewINavicareCodeLaravelFV faVsco.js~°9 master kProject© LiveCoachController.php© MissingTeamController.phpc) Mobilecontroller.ong© NotificationController.phpONowricationrrovidercontroller.onp© PlaybackConttroller.php© PlaylistController.php© PusherController.php© SlackController.php© SupportController.php© TeamSetupController.phpc) Userautomateakeporiscontroller.pnoc) welcomecontroller.onoU MicclewareSerializersM Transformers(C) Kernelohr©PlaylistTrackResourceTrait.phpT ValidateCrmConnectionReguiredtrait.oho• IntegrationsInteractionsJobsv Activity> Dialpad>D ImportO JustCallPushSummaryToCrm• D RingCentralm 7oomDhanc© ActivityChangeCategorylds.phpAssicnownersnip.onp© ConferenceCrmMatcherJob.phpC) DeleteActivities.php© DeleteTeamChurnData.phpC) DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpC)MatchMeetngowner.onvc) ReindexForAccount.o..ohoC) ReindexForContact.Job.ohvC) ReindexForGrouo.Job.ohoC) ReindexForLead.Job.ohnC) [EMAIL]) ReindexForUser.Job.oho(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nho(C) TeardownStream nhnM AiAutomationKeractor© BaseService.php© SoftPhoneManager.php© CoreUserRequest.phpconstants.ongy coreuser.pnpActivity/Close/service.pnp© Activity/RingCentral/Service.phpsyncacuivity.php165166167168215class SyncActivity extends Job implements ShouldQueueorivatetunction runo.ActivirvimoortResultsch1s->1mport->gectnovate,$this-›userRepository->findOneBy(['id' => $this->import->getUserIdO]),sch1s->1mport->gecAcc1V1cy100return new ActivitvimportResultoo->settotal SimportedRecords).->addImported($importedRecords)usayeprivate function complete(ActivityImportResult $result): voidSthis->activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog:: increment( stats: "jiminny.activity.sync.success','company' => $this->context['team'],'provider' => $this->context['provider'],sampleRate: 1.0, [D);$this->logger->info('[SyncActivity] End', $this->context);$this->logger->info("Lsyncaccivity. renory usage"arraymeroeu'memory usage => memory get usageo.'memoryreal usage' => memory qet usagec real usage: true)'pid' => getmypid(),orivate function faluumoortuhrowable Sexcention: void .?The Hunsnell nluain hac heon denrecated. If vou're not writing in Hungarian vou canEcustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]& console [EU]A1. Ydeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 € >146 @ >150151 G>1usadeorivate 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ßBeIncluded(Team $team): boolf...}CascadeCascadef Support Daily - in 4h 23mU AskJiminnyReportActivityServiceTest~100% Lz&• Tue 19 May 10:37:36+0 ..wCascade Code *•Kick off a new project. Make changesacross your entre codedase• SCIM Role Management Implementationc Salesforce Token Fallback© Fixing Redis Rate Limit ErrorAsk anvthina (884-L)WN Windsurf Toams 178-25UTF.8f?4 spaces...
|
56172
|
NULL
|
NULL
|
NULL
|
|
56213
|
1958
|
2
|
2026-05-19T07:43:37.257410+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176617257_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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","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":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4316251472625321264
|
-259430402414309411
|
app_switch
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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
|
|
56214
|
1959
|
2
|
2026-05-19T07:43:37.187926+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176617187_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":"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.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","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":false,"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
|
|
56217
|
1958
|
4
|
2026-05-19T07:43:42.370245+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176622370_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScripts|ProfilesWindow iTerm2ShellEditViewSessionScripts|ProfilesWindowHelpDOCKER• ₴1DEV (-zsh)₴82app/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/MeetingBot/Service/ParticipantMatcher.phpapp/Exceptions/RateLimitException.phpapp/Http/Middleware/McpTierMiddleware.phpapp/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpapp/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php1946192747422157app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.phpapp/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.phpapp/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php5971321521app/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.php251402195027396856tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php151tests/Unit/Services/Activity/HubSpot/ServiceTest.php109tests/Unit/Services/Crm/Hubspot/ClientTest.php250tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php l28337 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $Support Daily - in 4 h 17 mAPP (-zsh)|APP (-zsh)++++---++++++1++-++++++++++++-+-+-*3screenpipe"0 ₴4100% C4 8• Tue 19 May 10:43:42C*1-zsh*5i+++++++++++++++APP+++++++++++++++++++++++++++++++++++1++++++++--++++++++++++++++++++++++....
|
NULL
|
-1575118524083268803
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScripts|ProfilesWindow iTerm2ShellEditViewSessionScripts|ProfilesWindowHelpDOCKER• ₴1DEV (-zsh)₴82app/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/MeetingBot/Service/ParticipantMatcher.phpapp/Exceptions/RateLimitException.phpapp/Http/Middleware/McpTierMiddleware.phpapp/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpapp/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php1946192747422157app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.phpapp/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.phpapp/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php5971321521app/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.php251402195027396856tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php151tests/Unit/Services/Activity/HubSpot/ServiceTest.php109tests/Unit/Services/Crm/Hubspot/ClientTest.php250tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php l28337 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $Support Daily - in 4 h 17 mAPP (-zsh)|APP (-zsh)++++---++++++1++-++++++++++++-+-+-*3screenpipe"0 ₴4100% C4 8• Tue 19 May 10:43:42C*1-zsh*5i+++++++++++++++APP+++++++++++++++++++++++++++++++++++1++++++++--++++++++++++++++++++++++....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56218
|
1959
|
4
|
2026-05-19T07:43:42.362032+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176622362_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityLaterMoreSlackcalVIewMistonWindowhelpDescr ActivityLaterMoreSlackcalVIewMistonWindowhelpDescribe wnat you are looking forJiminny... ~. Vasil Vasilev( UnreadsMessagest Add canvasUr Files& Pins@ ThreadsLukas KoV: Yesterday6a HuddlesDrafts & sent8) Directories01Vasil Vasilev 10:40 AMScreenshot 2026-05-18 at 10.40.05.png ~Ab External connectionst Starred8 jiminny-x-integrati...& platform-inner-team# Channels# ai-chapterне знам лали го ползваш. но е многополезен тwулu alerts# backend# bugsi confusion-clinicVasil Vasilev 9:18 AMДобро утро, Лукаш# curiosity_lab# engineering# general# jiminny-bgкогато имаш лнес възможностмоля те погледни тоя ПР.nuos:/citnuo.com/lminnv/apo/oull1208/i nlatform-nicketsV1# product_launches# random# releases# sofia-office# support# thank-yousVasil Vasilley 10:29 AMолаголаnяMessage Vasil Vasilev+ Aa© DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpc)MatchMeetngowner.onvc) ReindexForAccount.oe.ohoC) ReindexForContact.Job.ohoC) ReindexForGrouo.Job.ohoC) ReindexForLead.Job.ohnC) [EMAIL]) ReindexForUser. Job.oho(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nhn(C) TeardownStream nhn> M AiAutomationM AiRenortsfkesolver.php© BaseService.php© ScimProvisioning.phpy coreuser.pnp© SoftPhoneManager.php© CoreUserRequest.php© Activity/Close/service.pnp© Activity/RingCentral/Service.phpvice.ohods Job implements ShouldQueueO: ActIvtvimoortResultt-›geccnovate,epository->findOneBy(['id' => $this->import->getUserIdO)]),c->gecAccIV1cy10vitvimportResultooamportedRecords)d($importedRecords)plete(ActivityImportResult $result): voidmportManager->complete($this->import, $result);nt( stats:'jiminny.activity.sync.success',sampleRate: 1.0, [$this->context['team'],> $this->context['provider'],nfo('[SyncActivity] End', $this->context);nfolcy. renory usage',ory usage => memory qet usageo.memory real usage => memory get usage real usage: true)'pid' => getmypid(),Ecustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]# console [euyA1. Ydeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 @>146 @>150151 G>1usadeorivate const int No GROUP 10 = 999private 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...}CascadeCascade7 & Support Daily - in 4 h 17 mU AskJiminnyReportActivityServiceTest~100% C4 & • Tue 19 May 10:43:42+0 ..Cascade Code *•Kick off a new project. Make changesacross your entire codedaseprivate function failimport(Throwable Sexception): voidt...}• SCIM Role Management Implementationc Salesforce Token Fallback© Fixing Redis Rate Limit ErrorAsk anvthina (884-L)« Code SWF-1.6W Windsurf Teamf 4 spaces...
|
NULL
|
6565306996837130942
|
NULL
|
click
|
ocr
|
NULL
|
ActivityLaterMoreSlackcalVIewMistonWindowhelpDescr ActivityLaterMoreSlackcalVIewMistonWindowhelpDescribe wnat you are looking forJiminny... ~. Vasil Vasilev( UnreadsMessagest Add canvasUr Files& Pins@ ThreadsLukas KoV: Yesterday6a HuddlesDrafts & sent8) Directories01Vasil Vasilev 10:40 AMScreenshot 2026-05-18 at 10.40.05.png ~Ab External connectionst Starred8 jiminny-x-integrati...& platform-inner-team# Channels# ai-chapterне знам лали го ползваш. но е многополезен тwулu alerts# backend# bugsi confusion-clinicVasil Vasilev 9:18 AMДобро утро, Лукаш# curiosity_lab# engineering# general# jiminny-bgкогато имаш лнес възможностмоля те погледни тоя ПР.nuos:/citnuo.com/lminnv/apo/oull1208/i nlatform-nicketsV1# product_launches# random# releases# sofia-office# support# thank-yousVasil Vasilley 10:29 AMолаголаnяMessage Vasil Vasilev+ Aa© DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpc)MatchMeetngowner.onvc) ReindexForAccount.oe.ohoC) ReindexForContact.Job.ohoC) ReindexForGrouo.Job.ohoC) ReindexForLead.Job.ohnC) [EMAIL]) ReindexForUser. Job.oho(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nhn(C) TeardownStream nhn> M AiAutomationM AiRenortsfkesolver.php© BaseService.php© ScimProvisioning.phpy coreuser.pnp© SoftPhoneManager.php© CoreUserRequest.php© Activity/Close/service.pnp© Activity/RingCentral/Service.phpvice.ohods Job implements ShouldQueueO: ActIvtvimoortResultt-›geccnovate,epository->findOneBy(['id' => $this->import->getUserIdO)]),c->gecAccIV1cy10vitvimportResultooamportedRecords)d($importedRecords)plete(ActivityImportResult $result): voidmportManager->complete($this->import, $result);nt( stats:'jiminny.activity.sync.success',sampleRate: 1.0, [$this->context['team'],> $this->context['provider'],nfo('[SyncActivity] End', $this->context);nfolcy. renory usage',ory usage => memory qet usageo.memory real usage => memory get usage real usage: true)'pid' => getmypid(),Ecustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]# console [euyA1. Ydeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 @>146 @>150151 G>1usadeorivate const int No GROUP 10 = 999private 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...}CascadeCascade7 & Support Daily - in 4 h 17 mU AskJiminnyReportActivityServiceTest~100% C4 & • Tue 19 May 10:43:42+0 ..Cascade Code *•Kick off a new project. Make changesacross your entire codedaseprivate function failimport(Throwable Sexception): voidt...}• SCIM Role Management Implementationc Salesforce Token Fallback© Fixing Redis Rate Limit ErrorAsk anvthina (884-L)« Code SWF-1.6W Windsurf Teamf 4 spaces...
|
56215
|
NULL
|
NULL
|
NULL
|
|
56219
|
1959
|
5
|
2026-05-19T07:43:44.312562+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176624312_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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":"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.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","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},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.29288563,"height":0.8818835},"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}]...
|
-6826177378374338903
|
-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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56220
|
1958
|
5
|
2026-05-19T07:43:44.629784+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176624629_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
[{"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","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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"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}]...
|
-8939325954395852202
|
-259430402414309411
|
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
<?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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
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...
|
56217
|
NULL
|
NULL
|
NULL
|
|
56240
|
1958
|
16
|
2026-05-19T07:46:50.679970+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176810679_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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
166739139053935259
|
-1414129818531492927
|
app_switch
|
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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56241
|
1959
|
15
|
2026-05-19T07:46:50.693956+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176810693_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":"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.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","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
|
app_switch
|
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
|
|
56242
|
1958
|
17
|
2026-05-19T07:46:55.172966+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176815172_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, 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":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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}]...
|
-4701279590415171657
|
-8204420343923494970
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
iTerm2ShellEditViewSessionScriptsProfilesWindowHelpSupport Daily • in 4 h 14 m100% C4 8• Tue 19 May 10:46:55APP (-zsh)|DOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpapp/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.phpAPP (-zsh)47++++++++++++--42+++++++++++++++2157++++++*3app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.php2app/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.phpapp/Services/Activity/HubSpot/Providers/Provider0rum.php22+-app/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.php97132++++++++15++++++21++++----225+++++++++140+++++++219++++++-50ттІTTTttTтттттттт27+++++++---39+++++++*6856tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php151109+++++=250Ttt283+++++37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandLeHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-report-related-objectsSwitched to a new branch 'JY-20676-delete-report-related-objects'Lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $Iscreenpipe"₴84-zsh85APP...
|
56240
|
NULL
|
NULL
|
NULL
|
|
56243
|
1959
|
16
|
2026-05-19T07:46:55.163741+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176815163_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, 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":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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}]...
|
-4701279590415171657
|
-8204420343923494970
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, 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 JY-20676-delete-report-related-objeProject© SoftPhoneManager.php© CoreUserRequest.php© LiveCoachController.php© MissingTeamController.phpc) Mobilecontroller.ong© NotificationController.phpOnourcationrrovidercontroller.onp© PlaybackConttroller.php© PlaylistController.php© PusherController.phpSlackController.php© SupportController.php© TeamSetupController.phpc) Userautomareakeporiscontroller.pnpc) welcomecontroller.onoMicclewareC RequestsSerializersM Transformers(C) Kernelohr©PlaylistTrackResourceTrait.phpT ValidateCrmConnectionReguiredTrait.ohoIntegrationsm InteractionsD JobsD Activity>D Dialpad> Mimnort•O JustCallPushSummaryToCrm• D RingCentralm 7oomDhanc© ActivityChangeCategorylds.phpAssignownersnip.onp© ConferenceCrmMatcherJob.php© DeleteActivities.php©DeleteTeamChurnData.php© DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpC)MatchMeetngowner.onv© ReindexForAccountJob.phpC) ReindexForContact.Job.ohvC) ReindexForGrouo.Job.ohvC) ReindexForLead.Job.ohnC) [EMAIL]© Constants.phpy coreuser.pnp© Activity/Close/service.pnp© Activity/RingCentral/Service.phpsyncactivily.phpclass SyncActivity extends Job implements ShouldQueueorivatetunction runo.ActivirvimoortResultsch1s->1mport->gectnovareo.$this-›userRepository->findOneBy(['id' => $this->import->getUserIdO]),sch1s->1mport->gecacclv1cy10165166215C) ReindexForUser. Job.oho(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nhn(C) TeardownStream nhnM AiAutomationreturn (new ActivityImportresulto)->settotal(SimportedRecords).->addImported($importedRecords)private function complete(ActivityImportResult $result): voidSthis->activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog:: increment( stats: "jiminny.activity.sync.success','company' => $this->context['team'],'provider' => $this->context['provider'],sampleRate: 1.0, [D);$this->logger->info('[SyncActivity] End', $this->context);$this->logger->info('[SyncActivity] Memory usage'array meroel'memory usage => memory get usageo'memory real usage' => memory qet usagec real_ usage: true).'pid' => getmypid(),orivate function faluumoortuhrowable Sexcention): void ..Ecustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]& console [EU]CascadeCascadeA1. Ydeclarelscrict_cypes=1)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 @>146 @>150151 G>1usadeorivate 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ßBeIncluded(Team $team): boolf...}"supoont Dally • In 4h 14mAskJiminnyReportActivityServiceTest100% Lz8• Tue 19 May 10:46:54e a t+0 ..wCascade Code *•Kick off a new project. Make changesacross your entre codedase• SCIM Role Management Implementation© Salesforce Token Fallback© Fixing Redis Rate Limit ErrorAsk anvthina (884-L)800WN Windsurf Teamc165•66UTE.8f?4 spaces...
|
56241
|
NULL
|
NULL
|
NULL
|
|
56244
|
1959
|
17
|
2026-05-19T07:47:11.843272+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779176831843_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncActivity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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}]...
|
-6870583155641010293
|
-8132287983061324858
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, 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:
PhostormVIewINavicarecodeLaravelKeractorFV faVsco.js~%9 JY-20676-delete-report-related-objeProject© SoftPhoneManager.php© CoreUserRequest.php© LiveCoachController.php© MissingTeamController.phpc) Mobilecontroller.ong© NotificationController.phpOnourcationrrovidercontroller.onp© PlaybackConttroller.php© PlaylistController.php© PusherController.phpSlackController.php© SupportController.php© TeamSetupController.phpc) Userautomareakeporiscontroller.pnpc) welcomecontroller.onoMicclewareC RequestsSerializersM Transformers(C) Kernelohr©PlaylistTrackResourceTrait.phpT ValidateCrmConnectionReguiredTrait.ohoIntegrationsm InteractionsD JobsD Activity>D Dialpad› D Import•O JustCallPushSummaryToCrm• D RingCentralm 7oomDhanc© ActivityChangeCategorylds.phpAssignownersnip.onp© ConferenceCrmMatcherJob.php© DeleteActivities.php©DeleteTeamChurnData.php© DeleteTeamsRetentionData.phpC) HardDeleteActivities.phg© HardDeleteActivity.phpC)MatchMeetngowner.onvc) ReindexForAccount.o..ohoC) ReindexForContact.Job.ohvC) ReindexForGrouo.Job.ohoC) ReindexForLead.Job.ohnC) [EMAIL]© Constants.phpy coreuser.pnp© Activity/Close/service.pnp© Activity/RingCentral/Service.phpsyncacuivity.php165166C) ReindexForUser. Job.oho215(c) RotrvActivitvSvne.loh.nhn(c) SvncActivitv nhn(C) TeardownStream nhnM AiAutomationclass SyncActivity extends Job implements ShouldQueueorivatetunction runo.ActivirvimoortResultsch1s->1mport->gectnovareo.$this-›userRepository->findOneBy(['id' => $this->import->getUserIdO]),sch1s->1mport->gecacclv1cy10return (new ActivityImportresulto)->settotal(SimportedRecords).->addImported($importedRecords)private function complete(ActivityImportResult $result): voidSthis->activitvimoortManager->comolete(Sthis->imoort. Sresult):Datadog:: increment( stats: "jiminny.activity.sync.success','company' => $this->context['team'],'provider' => $this->context['provider'],sampleRate: 1.0, [D);$this->logger->info('[SyncActivity] End', $this->context);$this->logger->info('[SyncActivity] Memory usage'array meroel'memory usage => memory get usageo'memory real usage' => memory qet usagec real_ usage: true).'pid' => getmypid(),orivate function faluumoortuhrowable Sexcention): void ..Ecustom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]& console [EU]declare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 @>146 @>150151 G>1usadeorivate 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...}CascadeCascade"suppont Dally • In 4h 13m100% 2&• Tue 19 May 10:47:11AskJiminnyReportActivityServiceTest+0 ..Cascade Code *•Kick off a new project. Make changesacross your entire codedase• SCIM Role Management Implementationc Salesforce Token Fallback© Fixing Redis Rate Limit ErrorWhen using ask jiminny reports|« Code SWF-1.6WN Windsurf Teams165•66UTE.8f?4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16912
|
756
|
1
|
2026-05-11T09:41:05.374695+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778492465374_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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":"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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}]...
|
5149590296267362150
|
-8708823570875044926
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
FinderFileEditViewGoWindowHelp•00DEV (docker)DOCKERO ₴1DEV (docker)882APP (-zsh)|• жзmasterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY]@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]labol# Support Daily - in 2 h 19 m-zsh84-zsh885100%8• Mon 11 May 12:41:051881screenpipe"O 86DEV...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16913
|
757
|
2
|
2026-05-11T09:41:05.362618+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778492465362_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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}]...
|
-961292259613483204
|
-8132362818571621434
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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:
PhostormcodeFV faVsco.jsroledeyg createnotes.ong© SyncRelatedActivityManager.phpС MаLсhACuViLies lONeWс маchаcиvilycrmbatae Noteoblect.onpС CпескAnакetrукemotematch.png~/Iminnyapo/apo/Joos/crm/Delere/Delerecrmenutytrai.ono© saveAcuivity.ongcsavelranscriouion.onc© SetupLayout.phpC) Client.phpphpidehelper.php©) PaqinationState.phoC) MatchCrmData.phpC) CrmObiectsResolver.phoc) SyncActivitv.phpC) ProviderRateLimiter.php©) PaqinationConfia.php© SyncFieldMetadata.phc) SvncHubspotObiects.rmay Sycentions)X P Cc W .*4I7:© SyncLeads.phpclass Macchacuivicvurmbara excenos Job 1mplemencs snoulouueue, shoulabeunlquec) SvncObiects.ohp© SvncOpportunities.lobc) suncoooortunitv.ono© SvncProfileMetadata.rC) SvncTeam=ields.Job.ollC)SvncTeamMetadata.ol© UpdateOpportunitySpC UodateStage.ongM noalRicksMailboxN MeetinaRotlM Middleware(C) HandleHubsnotPatel in(c) RateLimited.onoD StreamingD Teamleleononyv C Userc) ChangeLmailjob.pho© DeactivateUserJob.phweollotowAhowcinlowaillieor© SetupDefaultsavedSei 106SyncTolntercom.phpc) sunc o? anhat.onoC) SuncToUserPilot.ohoC BaseProcessina.Job.oho@ Dummv.Job.php© ImportRecallAlRecordings 1o4© ImportRemoteTrackJob.p 10*C.lob.nhn©.JobDispatcher.php© JobDispatcherInterface.p110@ PuraeSoftDeletedOpportu 111#. SasVicibilitvControl.nhnlv D Listenersv D ActivitiesvM ActivityDrovidor3m luctealiv MllcorDilot© TrackProviderin: 118D1V8AVpublic function backoffO: arrayreturn [30, 90, 180]:* achrows conzalnertxceptzoninterrace* achrows Notroundexceptzoninterrace* dchrows Exception IhrowablepubLic tunction handlelAcrvTvreDosttory sactvitvrenostrorv.urmactvirvservice scrmactzvitvservzceConnection Sconnection.): void {Sactivity = SactivitvRenository->FindBvidSthis->activitvid:if (Sactivity === null) {throw new_InvalidArqumentExceotionm'MatchActivitvermbatal Cannot find activitv.')Itry 1Sconnection->transactionfunctionOuse Sactivitv. ScrmActivitvService. SactivitvRenositorv) <Loa: • infod messaaeInemote coanchl =s Cthic-SnemnteSeanch.'set_configuration' => Sthis->fromConfiguration?-›getIdO,Iold ctatel =s [llload idi =s Cactivitv-saotlead(12.sa0+td^)|'contact id' => Sactivity->getContactO?->getIdO.'account id' => Sactivity->getAccountO?->getIdo'opportunity id' => Sactivity->getOpportunityO?->getIdO.'stage id' => Sactivity->getStageO?->getIdoSthis->resetcrmMapoingsSactivitv. SactivitvRevositorv*Sthis->switchcrmconfzqurat.ion-NeededSactiv1tv):lelner Code will hoin INF to underctand vour Laravel ann code II Generate II Don't Show Anvmore (todav Q•08)=laravel.logA SF (jiminny@localhost]4 HS_local (jiminny@localhost]# console [PKob.# console [euJ# console [slAGiNg)[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"neaders".?"Uace":L"Inu,or May 2020 14.21.15 6Ml"Jn"Transter-Encod1nq":"chunked")."Connection":"keep-alive""CF-Ray":L"9t80debödb60dcsa-S0F"J,"Strict-Transport-Secur1ty":"max-aqe=31536008* 1ncLudeSubDomains: preload")n"Vary":"orioin.accent-encodino""access-control-allow-credentials": ["false"J."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\","x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"Set-Cookie":["__cf_bm=SrUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.107-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"."","endpoints\":[{"unl".httns:la.nel.cloudflane.com./renon+W/v42c=NVA1cVTPQfVm32anS0#xVF/sd2RN"group\":\"cf-nel\","max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,"report to\":|"cf-nel\"."max age":604800}"]"Server": ["cloudflare"]}} {"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab""trace_10":C/AD8565-905t-4604-9405-0e5b551e5545"helsuppon Dally • In zn 15 m100% Lz• Mon 11 May 12:41:04AskJiminnyReportActivityServiceTest vCascade• HubSpot Rate Limi+0 ..SidsToVerifvl = sactivity->oet1d0:wreturn Sidstoverity:Then in DeleteContactloh:•handle.•phpSidsToVerify = []:Sdb→>transaction function O use .. &SidsToVerify: void <fall cleanunSidsToVerify = Sthis->handleActivities(Sactivities, Sdispatcher, Slogger):Dispatch AFTER commiforeach (sidsToVerify as &id)VerifyActivityCrmTaskJob::dispatch(Sid):Effort: small. Impact: correctness (no orphan verify jobs from rolled-back transactions) + shorter transactionsMy recommendationImplement all three, in this order of rol:1. Option 1 first (10 min effort, prevents data drift today)2. Option 3 second (15 min effort, fixes a subtle correctness bug)3. Option 2 when you tackle batch endpoints generallyAsking question• Which to imnlement now?Which to implement now?Ada HandlaHubspotRateLimit middleware + retryUntil + maxExceptions + RateLimitException-aware loggin to VerityActivitvCrmTaskJob, Minimal changeOntion 1 + Ontion 3correctness bug,All threeOntions 1 4 2 4 introduce a new VerifvActivitiecCrmTackc.loh (batch) larder seone• neede a new inh clace and batch verify in the CPM cervice laverNone — just discussDon't implement yet; want to think through the trade-offs more before changing anythingOther answe÷ « CodeClaude Onus 4.7 MediumWN Windsurf Teams48.6UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9853
|
446
|
2
|
2026-05-08T13:38:10.295667+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247490295_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5641617897080429754
|
-8160223333407913180
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
PhpStormVIewINavigarecodeFV faVsco.js°9 JY-20725-handle-HS-search-rate-limProiect v© HubspotLastModifiec© HubspotSingleSyncSC HubspotSyncStrategOProspectcache.pnpc huosporwebnookbav @ Pagination© HubspotPaginationS© PaginationConfia.ph/CneckAnaretrykemotematch.phg( RateLimitexced(C)MatchCrmData.phoC) CrmObiectsResolver.php(C) ProviderRateLimiter.phpC) PaqinationContia.php(c) PaqinationState.phpC7 ProspectSearchStrategyk?php>D Redisdeclare (strict tvoes=i).• W ServiceTraitsT Opportunitysynctralnamespace Jiminny Services Crm Hubsoot Servicetraits:TsunccrmentitestiratTsuncfieldstirait.ono(t) WriteCrmTrait.oho>use |• M Utils23 Ctrait SyncCrmEntitiesTraitN Webhook© BatchSyncCollector.php(c) RatchSvncRedicServiceuse OpportunitySyncTrait;— 18(c) Client nhnprivate const string CDN_URL ='[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date* Qparam Carbon|null $to Optional end date for modification range© DataClient.php© DecorateActivity.php@ LocalSearch.nhn* aneturn int Number of contacts successfully synced71 6public function syncContacts(Carbon Ssince, ?Carbon $to = null): int{...}@ localSearchinterface.nh© RemoteSearch.php© Service.phpv Mlictonors* dinherizdoc© ConvertLeadActivities.p 100 @l 6t ›public function svncContact(string ScrmId): ?Contactf...?= custom.log x= laravel.logA SF [jiminny@localhost]4 HS_local (jiminny@localhost)« console [PROD]A console leu)# console [SlAvING[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {5 X19 A V"neaders".?"Vace".L"Inu,or May 2020 14.21.15 6Ml"J"Loncent-lvpe". "applicacionson,charser=utt-o'"Transfer-Encoding": ["chunked"]."CF-Ray":["9f80deb8db60dc3a-SOF"]."CF-Cache-Status":L"DYNAMIC"J,"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncLudeSubDomains: preload")"server-timing": ["hcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfr;desc=|"9f80deb8e7c6dc3a-1AD\""],'x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],'Set-Cookie":["__cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW100.ufZ07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"•r"s"endnoints"."urz\":\"https:|\/\V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn\"group\":\"cf-nel\".\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,reporc to. "cr-nel,"max age":604800}"]."Server": ["cloudflare"]}} {"correlation_ 1d":"95256555-ec98-4541-b92a-adta/Sb69eab","trace_1d":"C/Ab8565-905f-4604-9405-0e5b551e5545"CascadeHubspot Rate LimitingHubSpot Rate Limit Regerclient()-gecengagementbatal)getClient()->updateEngagement ()getClient()->addAssociations()getClient()->removeAssociations()getClient()->updateMeeting()getClient()->getEngagementData()getClient()->deleteEngagement()getClient()->createEngagement()getClient()->createMeeting()getClient()->createEngagement()ServiceTraits/SyncCrmEntitiesTrait.phpclient->getAssociationsData($ids, from, to)client->getPaginatedDataGenerator($payload, 'contactsclient->getContactById()client->getPaginatedDataGenerator($payload, ' compaclient->getAccountById()client->getContactsByIds()client->aetComoaniesBvidsx2)Servicetra1ts/Ooportun1tvsvnctra1t.ohoCallclient->qetAssociationsData(Sdealids, 'deals', "companies')Servicerra1ts/Syncrieldstra1t.phpcollclient->getInstance()->companyProperties().all(lclient->getInstance()->contactProperties().all()client->getinstance()->dealProperties(.alu)client->getInstance()->objectProperties('call').all()client->getInstance()->objectProperties('meeting').all()client_saetInctance/l_sohfectPronertiec/itackt).all)Ask anvthing (8+D)S Adaptive100% 12Fri 8 May 16:38:13AskJiminnyReportActivityServiceTest vInvestigating Rate LinNew CascadeHubspot erm call RearyloAcavilyl) uine ~120)BURSTRupsTupsertActivity) (une ~1/2updateActivityAssociations() (line ~295)RLIPSTwupdateActivityAssociations() (line~303)RupSTupdateMeetingV3() (line ~326)RupSThasEngagementActivityType() (line ~332)RupSTupdateTask() (line ~352)createActivity() (line ~393)RUPSTcreateMeetingV3() (line ~484)saveFollowupActávity() (ine ~598)Method conteytLimktgetAssociationDataForCollection() (line ~38)BURSTsvncGontactsl) fline -82)SCAPCHIsyncSingleContact() (line ~108)BURSTsvncAccounts" (line ~406)SEARCHIsvncSingleAccount() (line ~431)BURSTbatchFetchContactso line ~564)BURSTbatchFetchComoanies@.batchFetchComnaniesForAssociationsoinesBURSTMethod contextRate LimitsyncOpportunitiesBatch line ~193BURSTsyncOpportunitiesBatch line ~276BURSTsyncOpportunitiesBatch() (line ~280BURSTsyncCrm0biects() (line ~520)puocyMathod contextDoto timitgetObiectFields() (line ~79)BURSTgetObjectFields() (line ~80)BURSTgetObjectFields() (line ~81)BURSTgetObjectFields() (line ~83)RUpSTgetObjectFields() (line ~84)RUpSTgetObjectFields() (line ~85)W Windsurf Teams 1:1 UTF-8 P 4 spaces...
|
9851
|
NULL
|
NULL
|
NULL
|
|
9854
|
446
|
3
|
2026-05-08T13:38:26.289749+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247506289_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.37632978,"top":0.09736632,"width":0.5728058,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.37632978,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.37632978,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.37632978,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.37632978,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.37632978,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.37632978,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.37632978,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.37632978,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.37632978,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.37632978,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.37632978,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.37632978,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.37632978,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.37632978,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.37632978,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.37632978,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.37632978,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.37632978,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.37632978,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.37632978,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.37632978,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.37632978,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.37632978,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.37632978,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"62","depth":4,"bounds":{"left":0.31848404,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.3307846,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.34275267,"top":0.19792499,"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.35006648,"top":0.19792499,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"bounds":{"left":0.12765957,"top":0.1963288,"width":0.32114363,"height":0.8036712},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4321081535914644542
|
5036038088370227430
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9857
|
446
|
5
|
2026-05-08T13:38:35.411324+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247515411_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.37632978,"top":0.09736632,"width":0.5728058,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.37632978,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.37632978,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.37632978,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.37632978,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.37632978,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.37632978,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.37632978,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.37632978,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.37632978,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.37632978,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.37632978,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.37632978,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.37632978,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.37632978,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.37632978,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.37632978,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.37632978,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.37632978,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.37632978,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.37632978,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.37632978,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.37632978,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.37632978,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.37632978,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"62","depth":4,"bounds":{"left":0.31848404,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.3307846,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.34275267,"top":0.19792499,"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.35006648,"top":0.19792499,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"bounds":{"left":0.12765957,"top":0.1963288,"width":0.32114363,"height":0.8036712},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4321081535914644542
|
5036038088370227430
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9858
|
445
|
2
|
2026-05-08T13:38:36.593834+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247516593_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","depth":4,"on_screen":true,"role_description":"text"}]...
|
-7653161416789303368
|
-8132358420524979262
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
iTerm2Shell Edit ViewSessionScripts|ProfilesWindowHelp‹ $0ladl100% C8APP (-zsh)DOCKERDEV (docker)882JY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPAPP (-zsh)-zshJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY]@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limitSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'Lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I• 84screenpipe*•$5-zshFri 8 May 16:38:39T₴1|₴6APP...
|
9856
|
NULL
|
NULL
|
NULL
|
|
9862
|
446
|
8
|
2026-05-08T13:38:58.312907+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247538312_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.37632978,"top":0.09736632,"width":0.5728058,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.37632978,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.37632978,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.37632978,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.37632978,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.37632978,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.37632978,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.37632978,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.37632978,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.37632978,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.37632978,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.37632978,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.37632978,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.37632978,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.37632978,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.37632978,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.37632978,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.37632978,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.37632978,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.37632978,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.37632978,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.37632978,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.37632978,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.37632978,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.37632978,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"62","depth":4,"bounds":{"left":0.31848404,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.3307846,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.34275267,"top":0.19792499,"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.35006648,"top":0.19792499,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4321081535914644542
|
5036038088370227430
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9861
|
NULL
|
NULL
|
NULL
|
|
9863
|
445
|
4
|
2026-05-08T13:38:58.312893+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247538312_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"62","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4321081535914644542
|
5036038088370227430
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9859
|
NULL
|
NULL
|
NULL
|
|
9867
|
445
|
7
|
2026-05-08T13:39:16.346733+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247556346_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"62","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4321081535914644542
|
5036038088370227430
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9870
|
446
|
11
|
2026-05-08T13:39:20.116420+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247560116_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.37632978,"top":0.09736632,"width":0.5728058,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.37632978,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.37632978,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.37632978,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.37632978,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.37632978,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.37632978,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.37632978,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.37632978,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.37632978,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.37632978,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.37632978,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.37632978,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.37632978,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.37632978,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.37632978,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.37632978,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.37632978,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.37632978,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.37632978,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.37632978,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.37632978,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.37632978,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.37632978,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.37632978,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"62","depth":4,"bounds":{"left":0.31848404,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.3307846,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.34275267,"top":0.19792499,"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.35006648,"top":0.19792499,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4321081535914644542
|
5036038088370227430
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9872
|
446
|
13
|
2026-05-08T13:39:32.206460+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778247572206_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.37632978,"top":0.09736632,"width":0.5728058,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.37632978,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.37632978,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.37632978,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.37632978,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.37632978,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.37632978,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.37632978,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.37632978,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.37632978,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.37632978,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.37632978,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.37632978,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.37632978,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.37632978,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.37632978,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.37632978,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.37632978,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.37632978,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.37632978,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.37632978,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.37632978,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.37632978,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.37632978,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.37632978,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"62","depth":4,"bounds":{"left":0.31848404,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.3307846,"top":0.19952115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.34275267,"top":0.19792499,"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.35006648,"top":0.19792499,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4321081535914644542
|
5036038088370227430
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, 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
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
62
32
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|