|
45404
|
1625
|
45
|
2026-05-14T14:34:58.916690+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769298916_m1.jpg...
|
PhpStorm
|
faVsco.js – AccountController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% C•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :8• Thu 14 May 17:34:58Q18Projectv+,> D Telephony• D Webhook› D Hubspot> D IntegrationAppSubscriptions© ActivityProviderController.pt© ActivityTranscriptionControl© BaseController.php® CalendarController.php© ReportController.phpSoftphoneWebhookControlle20© AbstractController.php• CommentContextinterface.php33© ConferencesOptinOutControllel35© Controller.php© ExportController.php36© FrontendController.phpT FrontendControllerTrait.php© GeocodingController.php37 (0Р >5051© HealthCheckController.php© LiveCoachController.php© MissingTeamController.php© MobileController.php52 (0g >6566© NotificationController.php© NotificationProviderController.f 67 Qg© PlaybackController.php© PlaylistController.php© PusherController.php© SlackController.php68697071© SupportController.php© TeamSetupController.php72 đ >© UserAutomatedReportsControll81© WelcomeController.php82> D MiddlewareD RequestsD Resources›Responses© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.logPlanhatService.php© AutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.log© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.phpSF [jiminny@localhost]© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.php© PlainTextDecorateActivity.phpA HS_local [jiminny@localho:WT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.php X.env.staging=.env4 console [QAI PROD] X© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php4 console [PROD]© HubspotPaginationService.php© HandleHubspotRateLimit.phpclass AccountController extends AbstractControllerpublic function__construct(prevace reduonty Loggerancertace+cogger,)f...3A console (EU]A6DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66217/** New Account / Company Created */POST /webhook/intapp/acc-addpublic function created(): JsonResponse{... }218219.id = 33;220221/** Existing Account / Company Updated */POST /webhook/intapp/acc-updpublic function updated(): JsonResponse{... }222223224id THEN •(own225/** Existing Account / Company Deleted */POST /webhook/intapp/acc-delpublic function deleted(): JsonResponse226227228229return $this->handleBulkDeletion(targetKey: 'externalCompanies', fallbackKey: self::TARGET_KEY, eventName230hubspot':2312321 usageprotected function deleteTarget(Team Steam, string $targetId): void{...}233 V= 11512582;234Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams69:33UTF-8Ca 4 spaces...
|
NULL
|
6009738359263312153
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% C•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :8• Thu 14 May 17:34:58Q18Projectv+,> D Telephony• D Webhook› D Hubspot> D IntegrationAppSubscriptions© ActivityProviderController.pt© ActivityTranscriptionControl© BaseController.php® CalendarController.php© ReportController.phpSoftphoneWebhookControlle20© AbstractController.php• CommentContextinterface.php33© ConferencesOptinOutControllel35© Controller.php© ExportController.php36© FrontendController.phpT FrontendControllerTrait.php© GeocodingController.php37 (0Р >5051© HealthCheckController.php© LiveCoachController.php© MissingTeamController.php© MobileController.php52 (0g >6566© NotificationController.php© NotificationProviderController.f 67 Qg© PlaybackController.php© PlaylistController.php© PusherController.php© SlackController.php68697071© SupportController.php© TeamSetupController.php72 đ >© UserAutomatedReportsControll81© WelcomeController.php82> D MiddlewareD RequestsD Resources›Responses© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.logPlanhatService.php© AutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.log© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.phpSF [jiminny@localhost]© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.php© PlainTextDecorateActivity.phpA HS_local [jiminny@localho:WT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.php X.env.staging=.env4 console [QAI PROD] X© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php4 console [PROD]© HubspotPaginationService.php© HandleHubspotRateLimit.phpclass AccountController extends AbstractControllerpublic function__construct(prevace reduonty Loggerancertace+cogger,)f...3A console (EU]A6DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66217/** New Account / Company Created */POST /webhook/intapp/acc-addpublic function created(): JsonResponse{... }218219.id = 33;220221/** Existing Account / Company Updated */POST /webhook/intapp/acc-updpublic function updated(): JsonResponse{... }222223224id THEN •(own225/** Existing Account / Company Deleted */POST /webhook/intapp/acc-delpublic function deleted(): JsonResponse226227228229return $this->handleBulkDeletion(targetKey: 'externalCompanies', fallbackKey: self::TARGET_KEY, eventName230hubspot':2312321 usageprotected function deleteTarget(Team Steam, string $targetId): void{...}233 V= 11512582;234Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams69:33UTF-8Ca 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45405
|
1625
|
46
|
2026-05-14T14:35:03.611087+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769303611_m1.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
private function importOpportunity($crmData): ?Opportunity
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
->orderBy('id')
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);
}
}
$recordType = null;
if (empty($crmData['RecordTypeId']) === false) {
/** @var ?RecordType $recordType */
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmData['RecordTypeId'])
->first();
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->whe...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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":"11","depth":4,"bounds":{"left":0.725,"top":0.3122222,"width":0.01875,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"130","depth":4,"bounds":{"left":0.74791664,"top":0.3122222,"width":0.025,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.77708334,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.79791665,"top":0.3122222,"width":0.02013889,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n private function importOpportunity($crmData): ?Opportunity\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id ?? null,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n private function importOpportunity($crmData): ?Opportunity\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id ?? null,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-15845258123973217
|
-7851939513083164217
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
private function importOpportunity($crmData): ?Opportunity
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
->orderBy('id')
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);
}
}
$recordType = null;
if (empty($crmData['RecordTypeId']) === false) {
/** @var ?RecordType $recordType */
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmData['RecordTypeId'])
->first();
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->whe...
|
45404
|
NULL
|
NULL
|
NULL
|
|
45406
|
1626
|
26
|
2026-05-14T14:35:00.703622+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769300703_m2.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterday at 13.3Yesterday at 13:36Yesterday at 13:36Yesterday at 13.30Yesterdav at 13:35Yesterday at 13:34Yesterday at 13:34Yesterdav at 13:3kYesterday at 13:33Yesterday at 13.32Yesterdav at 13:31resterday at 15:30Yesterdav at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28Yecterdav at 12:28resterday at 13-21Yesterdav at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13:25Voctorday at 12:01Yesterday at 13.23Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:19Yesterdav at 13:18Yesterday at 13-1/Yesterdav at 13:17Yesterday at 13:16Yesterday at 13:15Yecterdav at 12:15Yesterday at 13.14Yecterdav at 13:10Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:11Yesterday at 13.10Yecterdav at 12:10Yesterdav at 13:09Yesterdav at 13:08v Size23 KB9 KB13 Kb16 KE17 KB29 KB16 K:12 KB9 KEIKBn8 KBI37 KB10 KB7 KE8 KB9 KB8 KB172 KR14 KB13 KB9 KE18 KB12 KB10 KBI16 KB6 KB6KB12 KE23 KB8 KB16 K:11 KB20 KP34 KB10 KB7 K:5 KB11 KB26 KBMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movieMDSG-A movieMP2G-4 movieMPEG-4 movieMPEG"4 movieMPEG-4 movieMoEeh movioMPEG-4 movieMPEG-1 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEG.A movicMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEC A movidMPEG-4 movieMDEG-A movieMPEG-4 movieMPEG-4 movieMDEC.A moviaMPEG-4 movie111 KC102 KB88 KBMDEG-A moviaMPEG-4 movie59 KB1MPEG-A movie98 KB MPEG-4 movie97 KB66 K3MPEG-4 movieMDEG.A movioAAKP93 KB/8 KBMPEG-4 movie50 KгMDEG-A movid58 KBMPEG-4 movie27 KB7KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.49 GB availabld• Inu 14 May 1/.30:00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 г+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
3494965858311650733
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterday at 13.3Yesterday at 13:36Yesterday at 13:36Yesterday at 13.30Yesterdav at 13:35Yesterday at 13:34Yesterday at 13:34Yesterdav at 13:3kYesterday at 13:33Yesterday at 13.32Yesterdav at 13:31resterday at 15:30Yesterdav at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28Yecterdav at 12:28resterday at 13-21Yesterdav at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13:25Voctorday at 12:01Yesterday at 13.23Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:19Yesterdav at 13:18Yesterday at 13-1/Yesterdav at 13:17Yesterday at 13:16Yesterday at 13:15Yecterdav at 12:15Yesterday at 13.14Yecterdav at 13:10Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:11Yesterday at 13.10Yecterdav at 12:10Yesterdav at 13:09Yesterdav at 13:08v Size23 KB9 KB13 Kb16 KE17 KB29 KB16 K:12 KB9 KEIKBn8 KBI37 KB10 KB7 KE8 KB9 KB8 KB172 KR14 KB13 KB9 KE18 KB12 KB10 KBI16 KB6 KB6KB12 KE23 KB8 KB16 K:11 KB20 KP34 KB10 KB7 K:5 KB11 KB26 KBMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movieMDSG-A movieMP2G-4 movieMPEG-4 movieMPEG"4 movieMPEG-4 movieMoEeh movioMPEG-4 movieMPEG-1 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEG.A movicMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEC A movidMPEG-4 movieMDEG-A movieMPEG-4 movieMPEG-4 movieMDEC.A moviaMPEG-4 movie111 KC102 KB88 KBMDEG-A moviaMPEG-4 movie59 KB1MPEG-A movie98 KB MPEG-4 movie97 KB66 K3MPEG-4 movieMDEG.A movioAAKP93 KB/8 KBMPEG-4 movie50 KгMDEG-A movid58 KBMPEG-4 movie27 KB7KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.49 GB availabld• Inu 14 May 1/.30:00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 г+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45403
|
NULL
|
NULL
|
NULL
|
|
45407
|
1626
|
27
|
2026-05-14T14:35:07.411604+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769307411_m2.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
private function importOpportunity($crmData): ?Opportunity
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
->orderBy('id')
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);
}
}
$recordType = null;
if (empty($crmData['RecordTypeId']) === false) {
/** @var ?RecordType $recordType */
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmData['RecordTypeId'])
->first();
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->whe...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"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":"11","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"130","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n private function importOpportunity($crmData): ?Opportunity\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id ?? null,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","depth":4,"bounds":{"left":0.3949468,"top":0.3008779,"width":0.38597074,"height":0.6991221},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n private function importOpportunity($crmData): ?Opportunity\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id ?? null,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-15845258123973217
|
-7851939513083164217
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
private function importOpportunity($crmData): ?Opportunity
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
->orderBy('id')
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);
}
}
$recordType = null;
if (empty($crmData['RecordTypeId']) === false) {
/** @var ?RecordType $recordType */
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmData['RecordTypeId'])
->first();
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->whe...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45408
|
1625
|
47
|
2026-05-14T14:35:07.391809+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769307391_m1.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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":"11","depth":4,"bounds":{"left":0.725,"top":0.3122222,"width":0.01875,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"130","depth":4,"bounds":{"left":0.74791664,"top":0.3122222,"width":0.025,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.77708334,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.79791665,"top":0.3122222,"width":0.02013889,"height":0.02111111},"on_screen":true,"role_description":"text"}]...
|
2836300952657883901
|
-8204336369630082614
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestProject v+.© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php› MigrationPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.phpv Pipedrive> OpportunitySyncStrategy© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.php xT LogActivityTrait.php› ProspectSearchStrategy© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.phpPlainTextDecorateActivity.php© ApiFields.php© ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.php=.env.staging=.env18© Client.php© FieldDefinitions.php© DetachActivityObject.php© PipedriveApiClient.php© PipedriveApiException.php© Service.phpRematchActivityOnCrmObjectDetach.php© HubspotPaginationService.php© HandleHubspotRateLimit.phpclassService extends BaseServiceimplementsprivace const IIC CACHE_TIL = 000,MatchActivityCrmData.phpClient.php© TokenStorage.php80IZS124A11 A130 X3 X 21v O Salesforce1 usage› @ Fields> D OpportunityMatcherC OpportunitySyncStrategy> D ProspectSearchStrategyv D ServiceTraitsT BatchSyncTrait.php© FollowupActivityTrait.php© LogActivityTrait.phpT RecordManipulationsTraitSyncFieldsTrait.php125126127128129130 đ131132 C133134private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400/***/* Ovar Clientprotected Sclient;protected PayloadBuilder $payloadBuilder;protected QueryHandler $queryHandler;© Client.php3 usages© DecorateActivity.php135private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;• DeleteObjectsTrait.php136© FieldDefinitions.php137 6© PayloadBuilder.php138© Profile.php139© QueryBuilder.php140 đ© QueryHandler.php141© Querylterator.php142© QueryResults.php143public function__construct(Client Sclient,PayloadBuilder $payloadBuilder,protected Dispatcher $eventDispatcher,private readonly CountriesMap $countriesMap,private readonly ProspectPhotoPathService $prospectPhotoPathService,)6...3© Service.php156© SyncBatchRedisService.php157 O>public function getDisplayName(): stringf...}D Traits161C. RacePlient nhn120 At.nuhlinfunntinnaat Inhhalau.in+JWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)100% C8• Thu 14 May 17:35:06* :Q:=custom.log= laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66217- 218=219id = 33;— 220221222$223224id THEN •(own225226227228=229= 230hubspot':— 231- 232- 233 V= 11512582;234I I11W Windsurf Teams100:25UTF-8P. 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45409
|
1625
|
48
|
2026-05-14T14:35:18.551533+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769318551_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpablFV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest v100% <8• Thu 14 May 17:35:18* :QProject v+.•Х:-© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.phpwmieauonv PipedrivePlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> C OpportunitySyncStrategy© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.php xT LogActivityTrait.php> D ProspectSearchStrategy© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.phpPlainTextDecorateActivity.php© ApiFields.php© Client.phpT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.php=.env.staging=.env18© FieldDefinitions.php© DetachActivityObject.php© PipedriveApiClient.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© PipedriveApiException.php© HubspotPaginationService.php© HandleHubspotRateLimit.php© Service.php80classService extends BaseService implements© TokenStorage.php135A11 A130 X3 X21 ^private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;v D Salesforce136> • Fields137 ₫public function__construct(> OpportunityMatcher138Client Sclient,> D OpportunitySyncStrategy139PayloadBuilder $payloadBuilder,> D ProspectSearchStrategy140 đprotected Dispatcher $eventDispatcher,v D ServiceTraits141private readonly CountriesMap $countriesMap,T BatchSyncTrait.php142private readonly ProspectPhotoPathService $prospectPhotoPathService,( FollowupActivityTrait.php143) f...3T LogActivityTrait.php156+ RecordManipulationsTrait.p157 >public function getDisplayName(): string{...}T SyncFieldsTrait.php161© Client.php162 ›public function getJobDelay(): intf.© DecorateActivity.php166G DeleteObjectsTrait.php167 đ >protected function get0AuthAccount(User $user): ?SocialAccount{...}© FieldDefinitions.php171© PayloadBuilder.php172 € >public function verifyTaskExists(Activity $activity): bool{...}© Profile.php207© QueryBuilder.php208 0 >public function query(string $queryToRun, array $parameters = []): QueryIterator{...}© QueryHandler.php213© Querylterator.php214/*===Organization Information ======© QueryResults.php215© Service.php216/**© SyncBatchRedisService.php217* Get a list of all the API Versions for the instance.Traits218*© BaseClient.php219* Athrows CrmExceptionWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log= laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66id = 33;— 220221222$223224225226227228229- 230— 231- 232- 233 V234id THEN •(ownhubspot':= 11512582;I I11W Windsurf Teams100:25UTF-8Ca 4 spaces...
|
NULL
|
-5507277541688491363
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpablFV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest v100% <8• Thu 14 May 17:35:18* :QProject v+.•Х:-© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.phpwmieauonv PipedrivePlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> C OpportunitySyncStrategy© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.php xT LogActivityTrait.php> D ProspectSearchStrategy© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.phpPlainTextDecorateActivity.php© ApiFields.php© Client.phpT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.php=.env.staging=.env18© FieldDefinitions.php© DetachActivityObject.php© PipedriveApiClient.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© PipedriveApiException.php© HubspotPaginationService.php© HandleHubspotRateLimit.php© Service.php80classService extends BaseService implements© TokenStorage.php135A11 A130 X3 X21 ^private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;v D Salesforce136> • Fields137 ₫public function__construct(> OpportunityMatcher138Client Sclient,> D OpportunitySyncStrategy139PayloadBuilder $payloadBuilder,> D ProspectSearchStrategy140 đprotected Dispatcher $eventDispatcher,v D ServiceTraits141private readonly CountriesMap $countriesMap,T BatchSyncTrait.php142private readonly ProspectPhotoPathService $prospectPhotoPathService,( FollowupActivityTrait.php143) f...3T LogActivityTrait.php156+ RecordManipulationsTrait.p157 >public function getDisplayName(): string{...}T SyncFieldsTrait.php161© Client.php162 ›public function getJobDelay(): intf.© DecorateActivity.php166G DeleteObjectsTrait.php167 đ >protected function get0AuthAccount(User $user): ?SocialAccount{...}© FieldDefinitions.php171© PayloadBuilder.php172 € >public function verifyTaskExists(Activity $activity): bool{...}© Profile.php207© QueryBuilder.php208 0 >public function query(string $queryToRun, array $parameters = []): QueryIterator{...}© QueryHandler.php213© Querylterator.php214/*===Organization Information ======© QueryResults.php215© Service.php216/**© SyncBatchRedisService.php217* Get a list of all the API Versions for the instance.Traits218*© BaseClient.php219* Athrows CrmExceptionWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log= laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66id = 33;— 220221222$223224225226227228229- 230— 231- 232- 233 V234id THEN •(ownhubspot':= 11512582;I I11W Windsurf Teams100:25UTF-8Ca 4 spaces...
|
45408
|
NULL
|
NULL
|
NULL
|
|
45410
|
1626
|
28
|
2026-05-14T14:35:18.551547+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769318551_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pm~ (Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterday at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterday at 13:32resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterday at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterday at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yesterday at 13:12Yesterday at 13.11Yecterdav at 12:10Yesterday at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KBMPEG-4 movie23 KBMPEG-4 movieMPEG-4 movie13 KE16 KB17 KBMPE0"4 movie29 K:6 KBMPEG-4 movie12 KBMP2G-4 movieMPEG-4 movie7 KBI8 KB37 KBMPEG"4 movie10 KВIMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KBMPEG-4 movie72 KB14 KBMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KBMPEG-4 movie16 KB6KBMPEG-4 movie6KEMPEG-4 movie12 KBMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMPEG-4 movie11 KBMPEG-4 movie11 KPMPEG-4 movie20 KB34 KB10 K:MPEG-4 movie7KBMPEG-4 movie5 KB11 KBMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KBMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBGAKPMPEG-4 movieMPEG-4 movie44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• 0FavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderLocationsQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-yWJo.zio• Photos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pa• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВ148 KB122 KB111 KB94 KBZIP archivelZIP archiveXML documentAlfred...ferencesPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.48 GB availabld• Inu 14 May 1/.30.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
7850357533355687519
|
NULL
|
click
|
ocr
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pm~ (Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterday at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterday at 13:32resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterday at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterday at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yesterday at 13:12Yesterday at 13.11Yecterdav at 12:10Yesterday at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KBMPEG-4 movie23 KBMPEG-4 movieMPEG-4 movie13 KE16 KB17 KBMPE0"4 movie29 K:6 KBMPEG-4 movie12 KBMP2G-4 movieMPEG-4 movie7 KBI8 KB37 KBMPEG"4 movie10 KВIMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KBMPEG-4 movie72 KB14 KBMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KBMPEG-4 movie16 KB6KBMPEG-4 movie6KEMPEG-4 movie12 KBMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMPEG-4 movie11 KBMPEG-4 movie11 KPMPEG-4 movie20 KB34 KB10 K:MPEG-4 movie7KBMPEG-4 movie5 KB11 KBMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KBMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBGAKPMPEG-4 movieMPEG-4 movie44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• 0FavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderLocationsQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-yWJo.zio• Photos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pa• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВ148 KB122 KB111 KB94 KBZIP archivelZIP archiveXML documentAlfred...ferencesPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.48 GB availabld• Inu 14 May 1/.30.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45407
|
NULL
|
NULL
|
NULL
|
|
45411
|
1625
|
49
|
2026-05-14T14:35:20.839337+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769320839_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"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":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"}]...
|
-117723898423684695
|
-7123497198205957173
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl19100% C•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :8• Thu 14 May 17:35:20QProjectv© UserAutomatedReportsController.php:= custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php( SyncCrmEntitiesTrait.phpPlanhatService.phpT SyncFieldsTrait.php© ImportActivityTypes.phpT WriteCrmTrait.php> O Utils© Pipedrive/Service.php© AutomatedReportResult.php€ WriteCrmTrait.php xClose/Service.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.phpCopper/Service.phpBullhornService.php© PlainTextDecorateActivity.php> D Webhook© ActivityPlaybookTrait.php© CrmHelperRepository.php© AccountController.php=.env.staging=.env18© BatchSyncCollector.php© BatchSyncRedisService.php© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© Client.php© HandleHubspotRateLimit.php© ClosedDealStagesService.phpC DealFieldsService.php© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php• HubspotClientinterface.php© HubspotTokenManager.php© PayloadBuilder.php© RemoteCrmObjectManipulator.© ResponseNormalize.php© Service.php© SyncFieldAction.php© SyncRelatedActivityManager.p© WebhookSyncBatchProcessor.v D IntegrationApp> OJ Accessors> Api.› C Config> ODTO ×6 л// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderId()) {if (Sactivity->hasActualStartTime() && Sactivity->hasActualEndTime()) €Smetadata['startTime'] = Sdecorator->formatStartTime(Sactivity);$metadata['endTime'] = $decorator->formatEndTime(Sactivity);$metadata['title'] = $decorator-›generateTitle($activity);// For now, this is just a "meeting0utcome" field for somecustomers.Smetadata = array_merge($metadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK)):return $this->upsertActivity(Sactivity, $engagement, Smetadata);> O Filters2 usages> D Jobs.D ProspectSearchStrategy• Service Traits© DataClient.php© DecorateActivity.php© LocalSearch.php90106107108109110private function fetchCustomFieldData(Activity Sactivity, string $objectType): array{...}/*** @throws HubspotException* Othrows BadRequest*/• LocalSearchinterface.php3 usagesA DamntaCaarch nhn111private function upsertActivitv(Activity Sactivity. array $enqagement, array Smetadata): Activitví...}Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66— 217— 218|219.id = 33;220221222223224id THEN •(own225226227228229230hubspot':=231_232233V= 11512582;234W Windsurf Teams81:54UTF-8P. 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45412
|
1625
|
50
|
2026-05-14T14:35:31.919940+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769331919_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vlhl19HandleHubspotRateLimitTest100% C* :8• Thu 14 May 17:35:31QProjectv© UserAutomatedReportsController.php:= custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php( SyncCrmEntitiesTrait.phpPlanhatService.phpT SyncFieldsTrait.php© ImportActivityTypes.php© AutomatedReportResult.php€ WriteCrmTrait.php xSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.phpT WriteCrmTrait.php> O Utils© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.php XPlainTextDecorateActivity.php> D Webhook© ActivityPlaybookTrait.php© CrmHelperRepository.php© AccountController.php=.env.staging=.env18© BatchSyncCollector.php© BatchSyncRedisService.php© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© Client.php© HandleHubspotRateLimit.php© ClosedDealStagesService.phpC DealFieldsService.php© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php• HubspotClientinterface.php© HubspotTokenManager.php© PayloadBuilder.php© RemoteCrmObjectManipulator.© ResponseNormalize.php© Service.php© SyncFieldAction.php© SyncRelatedActivityManager.p© WebhookSyncBatchProcessor.v D IntegrationApp> OJ Accessors> Api.› C Config> ODTO ×6 л// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! Sactivity->hasCrmProviderId()) {if (Sactivity->hasActualStartTime() && Sactivity->hasActualEndTime()) {Smetadata['startTime') = $decorator->formatStartTime(Sactivity);Smetadata[ 'endTime'] = $decorator->formatEndTime(Sactivity);$metadata['title'] = $decorator->generateTitle($activity);}-// For now, this is just a "meetingOutcome" field for some customers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));return $this-›upsertActivity($activity, $engagement, $metadata);> O Filters2 usages> D Jobs.D ProspectSearchStrategy• Service Traits© DataClient.php© DecorateActivity.php© LocalSearch.php90106107108109110private function fetchCustomFieldData(Activity $activity, string $objectType): array{...}/*** @throws HubspotException* @throws BadRequest*/• LocalSearchinterface.php3 usagesA DamntaCaarch nhn111private function upsertActivity(Activity $activity, array $engagement, array Smetadata): Activity{...}Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66— 217— 218|219.id = 33;220221222223224id THEN '(own225226227228229230hubspot':=231_232233V= 11512582;234W Windsurf Teams81:54UTF-8P. 4 spaces...
|
NULL
|
4444662542388792437
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vlhl19HandleHubspotRateLimitTest100% C* :8• Thu 14 May 17:35:31QProjectv© UserAutomatedReportsController.php:= custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php( SyncCrmEntitiesTrait.phpPlanhatService.phpT SyncFieldsTrait.php© ImportActivityTypes.php© AutomatedReportResult.php€ WriteCrmTrait.php xSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.phpT WriteCrmTrait.php> O Utils© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.php XPlainTextDecorateActivity.php> D Webhook© ActivityPlaybookTrait.php© CrmHelperRepository.php© AccountController.php=.env.staging=.env18© BatchSyncCollector.php© BatchSyncRedisService.php© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© Client.php© HandleHubspotRateLimit.php© ClosedDealStagesService.phpC DealFieldsService.php© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php• HubspotClientinterface.php© HubspotTokenManager.php© PayloadBuilder.php© RemoteCrmObjectManipulator.© ResponseNormalize.php© Service.php© SyncFieldAction.php© SyncRelatedActivityManager.p© WebhookSyncBatchProcessor.v D IntegrationApp> OJ Accessors> Api.› C Config> ODTO ×6 л// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! Sactivity->hasCrmProviderId()) {if (Sactivity->hasActualStartTime() && Sactivity->hasActualEndTime()) {Smetadata['startTime') = $decorator->formatStartTime(Sactivity);Smetadata[ 'endTime'] = $decorator->formatEndTime(Sactivity);$metadata['title'] = $decorator->generateTitle($activity);}-// For now, this is just a "meetingOutcome" field for some customers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));return $this-›upsertActivity($activity, $engagement, $metadata);> O Filters2 usages> D Jobs.D ProspectSearchStrategy• Service Traits© DataClient.php© DecorateActivity.php© LocalSearch.php90106107108109110private function fetchCustomFieldData(Activity $activity, string $objectType): array{...}/*** @throws HubspotException* @throws BadRequest*/• LocalSearchinterface.php3 usagesA DamntaCaarch nhn111private function upsertActivity(Activity $activity, array $engagement, array Smetadata): Activity{...}Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66— 217— 218|219.id = 33;220221222223224id THEN '(own225226227228229230hubspot':=231_232233V= 11512582;234W Windsurf Teams81:54UTF-8P. 4 spaces...
|
45411
|
NULL
|
NULL
|
NULL
|
|
45413
|
1625
|
51
|
2026-05-14T14:35:34.446230+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769334446_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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":"6","depth":4,"bounds":{"left":0.78194445,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.80277777,"top":0.3122222,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"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\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"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":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"on_screen":false,"role_description":"text"}]...
|
4540741521732765890
|
5317234791027970645
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45414
|
1626
|
29
|
2026-05-14T14:35:31.904397+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769331904_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/•30.3)Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-7131983049336258321
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/•30.3)Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45415
|
1625
|
52
|
2026-05-14T14:35:51.055513+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769351055_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl19100% С8• Thu 14 May 17:35:50•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vU HandleHubspotRateLimitTest ~* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log> D JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php= laravel.log> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpPlainTextDecorateActivity.phpA HS_local [jiminny@localho:W© DecorateActivity.php© ActivityPlaybookTrait.php© CrmHelperRepository.php© AccountController.php=.env.staging=.env4 console [QAI PROD] X18© LocalSearch.php© LocalSearchinterface.php© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php4 console [PROD]© RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.phpA console (EU]15.04.26 Vasilev24trait LogActivityTraitv C Listeners86 ×1 лDGo jiminny21.04.26 Vasilev© ConvertLeadActivities.php46* Create or update a task or event (depending on most applicable playbook)084143 ×4 л21.04.26 Vasilev© PurgeLookupCache.php47*/— 215•m_layout_id =15.04.26 Vasilev48 Ctpublic function saveActivity(Activity $activity): Activity› Metadata15.04.26 Vasilev492162,1661,66799,66- 217> C Migration15.04.26 Vasilev50$playbook = $this->getPlaybookFromActivity(Sactivity);v 0 Pipedrive15.04.26 Vasilev51218> D OpportunitySyncStrategy21915.04.26 Vasilevif ($playbook === null) {> D ProspectSearchStrategy52id = 33;22020.04.26 Vasilev53throw new InvalidArgumentException( message: 'Please configure a Playbook first.© ApiFields.php-22215.04.26 Vasilev54© Client.php22215.04.26 Vasilev55© FieldDefinitions.php22327.04.26 Vasilev56$this->logger->info('[Salesforce] Log activity', [© PipedriveApiClient.php57224id THEN '(own27.04.26 Vasilev'activity_id' => $activity->getId),© PipedriveApiException.php22527.04.26 Vasilev© Service.php58'object_type' => $playbook->getActivityType(),27.04.26 Vasilev591):226© TokenStorage.php27.04.26 Vasilevv D Salesforce6022715.04.26 Vasilev22861$payload = [> O Fields22923.04.26 Vasilev> OpportunityMatcher62'OwnerId'=> $this-›profile-›getCrmProviderIdo,230hubspot':20.04.26 Vasilev63'Description' => Sthis->getActivityDecorator()->generateDescription(Sactivity) ~ 231-> D OpportunitySyncStrategy15.04.26 Vasilev64]:> D ProspectSearchStrategy23215.04.26 Vasilev65v • ServiceTraits233 V= 11512582;23.04.26 Vasilev66Spayload += Sthis->buildSubjectPayload(Sactivity, $playbook) ;T BatchSyncTrait.php23.04.26 Vasilev67234$payload += $this->buildProspectPayload($activity);( FollowupActivityTrait.php23.04.26 Vasilev68T LogActivityTrait.php23.04.26 Vasilev69$crm0bjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK+ RecordManipulationsTrait.p111T23.04.26 Vasilev70? Field::0BJECT_TASKT SyncFieldsTrait.php23.04.26 Vasilev71: Field: :0BJECT_EVENT;@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams63:56UTF-8Ca 4 spaces...
|
NULL
|
2855307501803612411
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl19100% С8• Thu 14 May 17:35:50•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vU HandleHubspotRateLimitTest ~* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log> D JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php= laravel.log> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpPlainTextDecorateActivity.phpA HS_local [jiminny@localho:W© DecorateActivity.php© ActivityPlaybookTrait.php© CrmHelperRepository.php© AccountController.php=.env.staging=.env4 console [QAI PROD] X18© LocalSearch.php© LocalSearchinterface.php© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php4 console [PROD]© RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.phpA console (EU]15.04.26 Vasilev24trait LogActivityTraitv C Listeners86 ×1 лDGo jiminny21.04.26 Vasilev© ConvertLeadActivities.php46* Create or update a task or event (depending on most applicable playbook)084143 ×4 л21.04.26 Vasilev© PurgeLookupCache.php47*/— 215•m_layout_id =15.04.26 Vasilev48 Ctpublic function saveActivity(Activity $activity): Activity› Metadata15.04.26 Vasilev492162,1661,66799,66- 217> C Migration15.04.26 Vasilev50$playbook = $this->getPlaybookFromActivity(Sactivity);v 0 Pipedrive15.04.26 Vasilev51218> D OpportunitySyncStrategy21915.04.26 Vasilevif ($playbook === null) {> D ProspectSearchStrategy52id = 33;22020.04.26 Vasilev53throw new InvalidArgumentException( message: 'Please configure a Playbook first.© ApiFields.php-22215.04.26 Vasilev54© Client.php22215.04.26 Vasilev55© FieldDefinitions.php22327.04.26 Vasilev56$this->logger->info('[Salesforce] Log activity', [© PipedriveApiClient.php57224id THEN '(own27.04.26 Vasilev'activity_id' => $activity->getId),© PipedriveApiException.php22527.04.26 Vasilev© Service.php58'object_type' => $playbook->getActivityType(),27.04.26 Vasilev591):226© TokenStorage.php27.04.26 Vasilevv D Salesforce6022715.04.26 Vasilev22861$payload = [> O Fields22923.04.26 Vasilev> OpportunityMatcher62'OwnerId'=> $this-›profile-›getCrmProviderIdo,230hubspot':20.04.26 Vasilev63'Description' => Sthis->getActivityDecorator()->generateDescription(Sactivity) ~ 231-> D OpportunitySyncStrategy15.04.26 Vasilev64]:> D ProspectSearchStrategy23215.04.26 Vasilev65v • ServiceTraits233 V= 11512582;23.04.26 Vasilev66Spayload += Sthis->buildSubjectPayload(Sactivity, $playbook) ;T BatchSyncTrait.php23.04.26 Vasilev67234$payload += $this->buildProspectPayload($activity);( FollowupActivityTrait.php23.04.26 Vasilev68T LogActivityTrait.php23.04.26 Vasilev69$crm0bjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK+ RecordManipulationsTrait.p111T23.04.26 Vasilev70? Field::0BJECT_TASKT SyncFieldsTrait.php23.04.26 Vasilev71: Field: :0BJECT_EVENT;@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams63:56UTF-8Ca 4 spaces...
|
45413
|
NULL
|
NULL
|
NULL
|
|
45416
|
1626
|
30
|
2026-05-14T14:35:51.059089+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769351059_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.48 GB availabld• Inu 14 May 1/.30.00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-8867762431077755168
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.48 GB availabld• Inu 14 May 1/.30.00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45414
|
NULL
|
NULL
|
NULL
|
|
45417
|
1625
|
53
|
2026-05-14T14:35:57.358985+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769357358_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5840353787124240911
|
-7195274138713911831
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vlhlU HandleHubspotRateLimitTest ~100% C8• Thu 14 May 17:35:56* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log› D JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php= laravel.log> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© DecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.phpBullhornService.php=.env.stagingPlainTextDecorateActivity.phpA HS_local [jiminny@localho:W18© LocalSearch.php© LocalSearchinterface.php© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.php=.envClient.php4 console [QAI PROD] X4 console [PROD]© RemoteSearch.php© Service.phpv D Listeners© ConvertLeadActivities.php© PurgeLookupCache.php› Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php© PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> O Fields> OpportunityMatcher> D OpportunitySyncStrategy> D ProspectSearchStrategy• Service TraitsT BatchSyncTrait.php( FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.pT SyncFieldsTrait.php© HubspotPaginationService.php15.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 VasilevA console (EU]244445464748 Ct © HandleHubspotRateLimit.phptrait LogActivityTrait* Save an activity, updateCRM prospect objects**/A6 X1 л* Create or update a task or event (depending on most applicable playbook)public function saveActivity(Activity $activity): Activityid = 33;$playbook = $this->getPlaybookFromActivity($activity);1):]:DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217218219220221if ($playbook === null) {222throw new InvalidArgumentException( message: 'Please configure a Playbook first.'-223224225226227— 228229230231=> $this->profile->getCrmProviderId(),232'Description' => $this->getActivityDecorator(->generateDescription(Sactivity),233 V234id THEN '(own$this->logger->info('[Salesforce] Log activity', ['activity_id' => $activity->getIdO.'object_type' => $playbook->getActivityType(),hubspat":$payload = ['OwnerId'= 11512582;Spayload += Sthis->buildSubjectPayload($activity, $playbook);$payload += $this->buildProspectPayload(Sactivity);@ Client nhnScrmObiectType = Splaybook->getActivityType( === Playbook::ACTIVITY_TYPE_TASKWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams66:38UTF-8Ca 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45418
|
1626
|
31
|
2026-05-14T14:35:57.358992+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769357358_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
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,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"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}]...
|
6814957943380363182
|
-8708761519200826421
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/.30:00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45419
|
1625
|
54
|
2026-05-14T14:35:59.916044+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769359916_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vlhlU HandleHubspotRateLimitTest ~100% C8• Thu 14 May 17:35:59* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log› D JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php= laravel.log> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© DecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.phpBullhornService.php=.env.stagingPlainTextDecorateActivity.phpA HS_local [jiminny@localho:W18© LocalSearch.php© LocalSearchinterface.php•DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.php=.envClient.php4 console [QAI PROD] X4 console [PROD]© RemoteSearch.php© Service.phpv D Listeners© ConvertLeadActivities.php© PurgeLookupCache.php› Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php© PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> O Fields> OpportunityMatcher> D OpportunitySyncStrategy> D ProspectSearchStrategy• Service TraitsT BatchSyncTrait.php( FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.pT SyncFieldsTrait.php© HubspotPaginationService.php15.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 VasilevA console (EU]244445464748 €t © HandleHubspotRateLimit.phptrait LogActivityTrait* Save an activity, updateCRM prospect objects**/A6 X1 л* Create or update a task or event (depending on most applicable playbook)public function saveActivity(Activity $activity): Activityid = 33;$playbook = $this->getPlaybookFromActivity($activity);1):]:DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217— 218219220221if ($playbook === null) {222throw new InvalidArgumentException( message: 'Please configure a Playbook first.'- V234id THEN '(own$this->logger->info('[Salesforce] Log activity', ['activity_id' => Sactivity->getIdO,'object_type' => $playbook->getActivityType(),hubspat":$payload = ['OwnerId'=> $this->profile->getCrmProviderId(),'Description' => $this->getActivityDecorator(->generateDescription(Sactivity),= 11512582;$payload += Sthis->buildSubjectPayload($activity, $playbook);$payload += $this->buildProspectPayload(Sactivity);=@ Client nhnScrmObiectType = Splaybook->getActivityType( === Playbook::ACTIVITY_TYPE_TASKWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams50:39UTF-8Ca 4 spaces...
|
NULL
|
7007481334674865461
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vlhlU HandleHubspotRateLimitTest ~100% C8• Thu 14 May 17:35:59* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log› D JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php= laravel.log> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© DecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.phpBullhornService.php=.env.stagingPlainTextDecorateActivity.phpA HS_local [jiminny@localho:W18© LocalSearch.php© LocalSearchinterface.php•DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.php=.envClient.php4 console [QAI PROD] X4 console [PROD]© RemoteSearch.php© Service.phpv D Listeners© ConvertLeadActivities.php© PurgeLookupCache.php› Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php© PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> O Fields> OpportunityMatcher> D OpportunitySyncStrategy> D ProspectSearchStrategy• Service TraitsT BatchSyncTrait.php( FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.pT SyncFieldsTrait.php© HubspotPaginationService.php15.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 VasilevA console (EU]244445464748 €t © HandleHubspotRateLimit.phptrait LogActivityTrait* Save an activity, updateCRM prospect objects**/A6 X1 л* Create or update a task or event (depending on most applicable playbook)public function saveActivity(Activity $activity): Activityid = 33;$playbook = $this->getPlaybookFromActivity($activity);1):]:DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217— 218219220221if ($playbook === null) {222throw new InvalidArgumentException( message: 'Please configure a Playbook first.'- V234id THEN '(own$this->logger->info('[Salesforce] Log activity', ['activity_id' => Sactivity->getIdO,'object_type' => $playbook->getActivityType(),hubspat":$payload = ['OwnerId'=> $this->profile->getCrmProviderId(),'Description' => $this->getActivityDecorator(->generateDescription(Sactivity),= 11512582;$payload += Sthis->buildSubjectPayload($activity, $playbook);$payload += $this->buildProspectPayload(Sactivity);=@ Client nhnScrmObiectType = Splaybook->getActivityType( === Playbook::ACTIVITY_TYPE_TASKWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams50:39UTF-8Ca 4 spaces...
|
45417
|
NULL
|
NULL
|
NULL
|
|
45420
|
1626
|
32
|
2026-05-14T14:36:02.164409+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769362164_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
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.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2586234742233688171
|
-7051436856496256053
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/.30:04Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45418
|
NULL
|
NULL
|
NULL
|
|
45421
|
1626
|
33
|
2026-05-14T14:36:06.630276+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769366630_m2.jpg...
|
PhpStorm
|
faVsco.js – Playbook.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Jiminny\Component\Eloquent\Builder;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Layout;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Jiminny\Models\Playbook
*
* @property int $id
* @property mixed $uuid
* @property int $team_id
* @property string $activity_type
* @property int|null $activity_field_id
* @property string $name
* @property bool $is_selectable
* @property bool $ai_activity_type_detection_enabled
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read Field|null $activityField
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\PlaybookCategory> $categories
* @property-read int|null $categories_count
* @property-read string $id_string
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Group> $groups
* @property-read int|null $groups_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Layout> $layouts
* @property-read int|null $layouts_count
* @property-read \Jiminny\Models\Team $team
* @property-read int|null $templates_count
*
* @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\PlaybookFactory factory(...$parameters)
* @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)
* @method static Builder|Playbook newModelQuery()
* @method static Builder|Playbook newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook onlyTrashed()
* @method static Builder|Playbook query()
* @method static Builder|Playbook selectable($is_selectable = true)
* @method static Builder|Playbook uuid(string $uuid, bool $first = true)
* @method static Builder|Playbook whereActivityFieldId($value)
* @method static Builder|Playbook whereActivityType($value)
* @method static Builder|Playbook whereCreatedAt($value)
* @method static Builder|Playbook whereDeletedAt($value)
* @method static Builder|Playbook whereId($value)
* @method static Builder|Playbook whereIsSelectable($value)
* @method static Builder|Playbook whereName($value)
* @method static Builder|Playbook whereTeamId($value)
* @method static Builder|Playbook whereUpdatedAt($value)
* @method static Builder|Playbook whereUuid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withoutTrashed()
*
* @mixin \Eloquent
*/
class Playbook extends Model
{
use HasFactory;
use RequiresUUID;
use Enums;
use SoftDeletes;
public const string ACTIVITY_TYPE_TASK = 'task';
public const string ACTIVITY_TYPE_EVENT = 'event';
public static array $enumActivityTypes = [
self::ACTIVITY_TYPE_TASK,
self::ACTIVITY_TYPE_EVENT,
];
protected $table = 'playbooks';
protected $fillable = [
'name',
'team_id',
'activity_type',
'activity_field_id',
'is_selectable',
'ai_activity_type_detection_enabled',
];
protected $casts = [
'ai_activity_type_detection_enabled' => 'boolean',
];
protected $appends = [
'id_string',
];
protected $hidden = [
'uuid',
'is_selectable',
'id',
'team_id',
];
protected function casts(): array
{
return [
'is_selectable' => 'boolean',
];
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function layouts(): BelongsToMany
{
return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();
}
public function categories(): HasMany
{
return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');
}
public function groups(): HasMany
{
return $this->hasMany(Group::class)->orderBy('name', 'asc');
}
public function activityField(): BelongsTo
{
return $this->belongsTo(Field::class);
}
#[Scope]
protected function selectable($query, $isSelectable = true)
{
return $query->where('is_selectable', $isSelectable);
}
public function isSelectable(): bool
{
return $this->getAttribute('is_selectable');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
public function getGroups(): Eloquent\Collection
{
return $this->getAttribute('groups');
}
public function hasTeam(): bool
{
return $this->getAttribute('team') !== null;
}
public function getName(): string
{
return $this->getAttribute('name');
}
public function getId(): int
{
return $this->getAttribute('id');
}
public function getActivityType(): string
{
return $this->getAttribute('activity_type');
}
public function getActivityField(): ?Field
{
/**
* @var Field
*/
return $this->getAttribute('activityField');
}
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
public function getCategories(): Eloquent\Collection
{
return $this->getAttribute('categories');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
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.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"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":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Jiminny\\Component\\Eloquent\\Builder;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\n/**\n * Jiminny\\Models\\Playbook\n *\n * @property int $id\n * @property mixed $uuid\n * @property int $team_id\n * @property string $activity_type\n * @property int|null $activity_field_id\n * @property string $name\n * @property bool $is_selectable\n * @property bool $ai_activity_type_detection_enabled\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property-read Field|null $activityField\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\PlaybookCategory> $categories\n * @property-read int|null $categories_count\n * @property-read string $id_string\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Group> $groups\n * @property-read int|null $groups_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Layout> $layouts\n * @property-read int|null $layouts_count\n * @property-read \\Jiminny\\Models\\Team $team\n * @property-read int|null $templates_count\n *\n * @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\PlaybookFactory factory(...$parameters)\n * @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)\n * @method static Builder|Playbook newModelQuery()\n * @method static Builder|Playbook newQuery()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook onlyTrashed()\n * @method static Builder|Playbook query()\n * @method static Builder|Playbook selectable($is_selectable = true)\n * @method static Builder|Playbook uuid(string $uuid, bool $first = true)\n * @method static Builder|Playbook whereActivityFieldId($value)\n * @method static Builder|Playbook whereActivityType($value)\n * @method static Builder|Playbook whereCreatedAt($value)\n * @method static Builder|Playbook whereDeletedAt($value)\n * @method static Builder|Playbook whereId($value)\n * @method static Builder|Playbook whereIsSelectable($value)\n * @method static Builder|Playbook whereName($value)\n * @method static Builder|Playbook whereTeamId($value)\n * @method static Builder|Playbook whereUpdatedAt($value)\n * @method static Builder|Playbook whereUuid($value)\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withTrashed()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Playbook extends Model\n{\n use HasFactory;\n\n use RequiresUUID;\n use Enums;\n use SoftDeletes;\n\n public const string ACTIVITY_TYPE_TASK = 'task';\n public const string ACTIVITY_TYPE_EVENT = 'event';\n\n public static array $enumActivityTypes = [\n self::ACTIVITY_TYPE_TASK,\n self::ACTIVITY_TYPE_EVENT,\n ];\n\n protected $table = 'playbooks';\n\n protected $fillable = [\n 'name',\n 'team_id',\n 'activity_type',\n 'activity_field_id',\n 'is_selectable',\n 'ai_activity_type_detection_enabled',\n ];\n\n protected $casts = [\n 'ai_activity_type_detection_enabled' => 'boolean',\n ];\n\n protected $appends = [\n 'id_string',\n ];\n\n protected $hidden = [\n 'uuid',\n 'is_selectable',\n 'id',\n 'team_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'is_selectable' => 'boolean',\n ];\n }\n\n public function team(): BelongsTo\n {\n return $this->belongsTo(Team::class);\n }\n\n public function layouts(): BelongsToMany\n {\n return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();\n }\n\n public function categories(): HasMany\n {\n return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');\n }\n\n public function groups(): HasMany\n {\n return $this->hasMany(Group::class)->orderBy('name', 'asc');\n }\n\n public function activityField(): BelongsTo\n {\n return $this->belongsTo(Field::class);\n }\n\n #[Scope]\n protected function selectable($query, $isSelectable = true)\n {\n return $query->where('is_selectable', $isSelectable);\n }\n\n public function isSelectable(): bool\n {\n return $this->getAttribute('is_selectable');\n }\n\n public function getTeam(): Team\n {\n return $this->getAttribute('team');\n }\n\n public function getTeamId(): int\n {\n return $this->getAttribute('team_id');\n }\n\n public function getGroups(): Eloquent\\Collection\n {\n return $this->getAttribute('groups');\n }\n\n public function hasTeam(): bool\n {\n return $this->getAttribute('team') !== null;\n }\n\n public function getName(): string\n {\n return $this->getAttribute('name');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getActivityType(): string\n {\n return $this->getAttribute('activity_type');\n }\n\n public function getActivityField(): ?Field\n {\n /**\n * @var Field\n */\n return $this->getAttribute('activityField');\n }\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getCategories(): Eloquent\\Collection\n {\n return $this->getAttribute('categories');\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Jiminny\\Component\\Eloquent\\Builder;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\n/**\n * Jiminny\\Models\\Playbook\n *\n * @property int $id\n * @property mixed $uuid\n * @property int $team_id\n * @property string $activity_type\n * @property int|null $activity_field_id\n * @property string $name\n * @property bool $is_selectable\n * @property bool $ai_activity_type_detection_enabled\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property-read Field|null $activityField\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\PlaybookCategory> $categories\n * @property-read int|null $categories_count\n * @property-read string $id_string\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Group> $groups\n * @property-read int|null $groups_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Layout> $layouts\n * @property-read int|null $layouts_count\n * @property-read \\Jiminny\\Models\\Team $team\n * @property-read int|null $templates_count\n *\n * @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\PlaybookFactory factory(...$parameters)\n * @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)\n * @method static Builder|Playbook newModelQuery()\n * @method static Builder|Playbook newQuery()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook onlyTrashed()\n * @method static Builder|Playbook query()\n * @method static Builder|Playbook selectable($is_selectable = true)\n * @method static Builder|Playbook uuid(string $uuid, bool $first = true)\n * @method static Builder|Playbook whereActivityFieldId($value)\n * @method static Builder|Playbook whereActivityType($value)\n * @method static Builder|Playbook whereCreatedAt($value)\n * @method static Builder|Playbook whereDeletedAt($value)\n * @method static Builder|Playbook whereId($value)\n * @method static Builder|Playbook whereIsSelectable($value)\n * @method static Builder|Playbook whereName($value)\n * @method static Builder|Playbook whereTeamId($value)\n * @method static Builder|Playbook whereUpdatedAt($value)\n * @method static Builder|Playbook whereUuid($value)\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withTrashed()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Playbook extends Model\n{\n use HasFactory;\n\n use RequiresUUID;\n use Enums;\n use SoftDeletes;\n\n public const string ACTIVITY_TYPE_TASK = 'task';\n public const string ACTIVITY_TYPE_EVENT = 'event';\n\n public static array $enumActivityTypes = [\n self::ACTIVITY_TYPE_TASK,\n self::ACTIVITY_TYPE_EVENT,\n ];\n\n protected $table = 'playbooks';\n\n protected $fillable = [\n 'name',\n 'team_id',\n 'activity_type',\n 'activity_field_id',\n 'is_selectable',\n 'ai_activity_type_detection_enabled',\n ];\n\n protected $casts = [\n 'ai_activity_type_detection_enabled' => 'boolean',\n ];\n\n protected $appends = [\n 'id_string',\n ];\n\n protected $hidden = [\n 'uuid',\n 'is_selectable',\n 'id',\n 'team_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'is_selectable' => 'boolean',\n ];\n }\n\n public function team(): BelongsTo\n {\n return $this->belongsTo(Team::class);\n }\n\n public function layouts(): BelongsToMany\n {\n return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();\n }\n\n public function categories(): HasMany\n {\n return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');\n }\n\n public function groups(): HasMany\n {\n return $this->hasMany(Group::class)->orderBy('name', 'asc');\n }\n\n public function activityField(): BelongsTo\n {\n return $this->belongsTo(Field::class);\n }\n\n #[Scope]\n protected function selectable($query, $isSelectable = true)\n {\n return $query->where('is_selectable', $isSelectable);\n }\n\n public function isSelectable(): bool\n {\n return $this->getAttribute('is_selectable');\n }\n\n public function getTeam(): Team\n {\n return $this->getAttribute('team');\n }\n\n public function getTeamId(): int\n {\n return $this->getAttribute('team_id');\n }\n\n public function getGroups(): Eloquent\\Collection\n {\n return $this->getAttribute('groups');\n }\n\n public function hasTeam(): bool\n {\n return $this->getAttribute('team') !== null;\n }\n\n public function getName(): string\n {\n return $this->getAttribute('name');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getActivityType(): string\n {\n return $this->getAttribute('activity_type');\n }\n\n public function getActivityField(): ?Field\n {\n /**\n * @var Field\n */\n return $this->getAttribute('activityField');\n }\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getCategories(): Eloquent\\Collection\n {\n return $this->getAttribute('categories');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"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":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.28224733,"top":1.0,"width":0.024268618,"height":-0.04788506},"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}]...
|
-161723684282621552
|
4668716167454726109
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Jiminny\Component\Eloquent\Builder;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Layout;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Jiminny\Models\Playbook
*
* @property int $id
* @property mixed $uuid
* @property int $team_id
* @property string $activity_type
* @property int|null $activity_field_id
* @property string $name
* @property bool $is_selectable
* @property bool $ai_activity_type_detection_enabled
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read Field|null $activityField
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\PlaybookCategory> $categories
* @property-read int|null $categories_count
* @property-read string $id_string
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Group> $groups
* @property-read int|null $groups_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Layout> $layouts
* @property-read int|null $layouts_count
* @property-read \Jiminny\Models\Team $team
* @property-read int|null $templates_count
*
* @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\PlaybookFactory factory(...$parameters)
* @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)
* @method static Builder|Playbook newModelQuery()
* @method static Builder|Playbook newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook onlyTrashed()
* @method static Builder|Playbook query()
* @method static Builder|Playbook selectable($is_selectable = true)
* @method static Builder|Playbook uuid(string $uuid, bool $first = true)
* @method static Builder|Playbook whereActivityFieldId($value)
* @method static Builder|Playbook whereActivityType($value)
* @method static Builder|Playbook whereCreatedAt($value)
* @method static Builder|Playbook whereDeletedAt($value)
* @method static Builder|Playbook whereId($value)
* @method static Builder|Playbook whereIsSelectable($value)
* @method static Builder|Playbook whereName($value)
* @method static Builder|Playbook whereTeamId($value)
* @method static Builder|Playbook whereUpdatedAt($value)
* @method static Builder|Playbook whereUuid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withoutTrashed()
*
* @mixin \Eloquent
*/
class Playbook extends Model
{
use HasFactory;
use RequiresUUID;
use Enums;
use SoftDeletes;
public const string ACTIVITY_TYPE_TASK = 'task';
public const string ACTIVITY_TYPE_EVENT = 'event';
public static array $enumActivityTypes = [
self::ACTIVITY_TYPE_TASK,
self::ACTIVITY_TYPE_EVENT,
];
protected $table = 'playbooks';
protected $fillable = [
'name',
'team_id',
'activity_type',
'activity_field_id',
'is_selectable',
'ai_activity_type_detection_enabled',
];
protected $casts = [
'ai_activity_type_detection_enabled' => 'boolean',
];
protected $appends = [
'id_string',
];
protected $hidden = [
'uuid',
'is_selectable',
'id',
'team_id',
];
protected function casts(): array
{
return [
'is_selectable' => 'boolean',
];
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function layouts(): BelongsToMany
{
return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();
}
public function categories(): HasMany
{
return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');
}
public function groups(): HasMany
{
return $this->hasMany(Group::class)->orderBy('name', 'asc');
}
public function activityField(): BelongsTo
{
return $this->belongsTo(Field::class);
}
#[Scope]
protected function selectable($query, $isSelectable = true)
{
return $query->where('is_selectable', $isSelectable);
}
public function isSelectable(): bool
{
return $this->getAttribute('is_selectable');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
public function getGroups(): Eloquent\Collection
{
return $this->getAttribute('groups');
}
public function hasTeam(): bool
{
return $this->getAttribute('team') !== null;
}
public function getName(): string
{
return $this->getAttribute('name');
}
public function getId(): int
{
return $this->getAttribute('id');
}
public function getActivityType(): string
{
return $this->getAttribute('activity_type');
}
public function getActivityField(): ?Field
{
/**
* @var Field
*/
return $this->getAttribute('activityField');
}
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
public function getCategories(): Eloquent\Collection
{
return $this->getAttribute('categories');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45423
|
1625
|
56
|
2026-05-14T14:36:08.175661+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769368175_m1.jpg...
|
PhpStorm
|
faVsco.js – Playbook.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestv8• Thu 14 May 17:36:08* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php> Jobs.PlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php© DataClient.phpC Playbook.php X© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.php© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging18© LocalSearch.php© LocalSearchinterface.phpE.env© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© RemoteSearch.php© Service.php© HubspotPaginationService.phpclass Playbook extends ModelHandleHubspotRateLimit.phpv D Listeners© ConvertLeadActivities.php© PurgeLookupCache.php› Metadata> C Migrationv [ Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php© PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> • Fields> OpportunityMatcherOpportunitySyncStrategy> D ProspectSearchStrategyv • ServiceTraits A6public function getGroups(): Eloquent \Collection{...}public function hasTeam(): boolf...}public function getName(: string{...}public function getId(): int{...}public function getActivityType(): string{return $this->getAttribute( key: 'activity_type');public function getActivityField(): ?Field{...}public function getUuid(): string{...}public function getCategories(): Eloquent\Collection{...}T BatchSyncTrait.php( FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.pT SyncFieldsTrait.php@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v4143 ×4 л215|•m_layout_id =2162,1661,66799,66E217=218219-id = 33;— 220221222=223224id THEN •(own225226227228229230hubspot':-231232=233 V= 11512582;— 234|W Windsurf Teams182:21UTF-8Ca 4 spaces...
|
NULL
|
-225936720231251789
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestv8• Thu 14 May 17:36:08* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php> Jobs.PlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php© DataClient.phpC Playbook.php X© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.php© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging18© LocalSearch.php© LocalSearchinterface.phpE.env© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© RemoteSearch.php© Service.php© HubspotPaginationService.phpclass Playbook extends ModelHandleHubspotRateLimit.phpv D Listeners© ConvertLeadActivities.php© PurgeLookupCache.php› Metadata> C Migrationv [ Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php© PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> • Fields> OpportunityMatcherOpportunitySyncStrategy> D ProspectSearchStrategyv • ServiceTraits A6public function getGroups(): Eloquent \Collection{...}public function hasTeam(): boolf...}public function getName(: string{...}public function getId(): int{...}public function getActivityType(): string{return $this->getAttribute( key: 'activity_type');public function getActivityField(): ?Field{...}public function getUuid(): string{...}public function getCategories(): Eloquent\Collection{...}T BatchSyncTrait.php( FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.pT SyncFieldsTrait.php@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v4143 ×4 л215|•m_layout_id =2162,1661,66799,66E217=218219-id = 33;— 220221222=223224id THEN •(own225226227228229230hubspot':-231232=233 V= 11512582;— 234|W Windsurf Teams182:21UTF-8Ca 4 spaces...
|
45422
|
NULL
|
NULL
|
NULL
|
|
45424
|
1626
|
34
|
2026-05-14T14:36:07.867668+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769367867_m2.jpg...
|
PhpStorm
|
faVsco.js – Playbook.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
Jiminny... ~ActivityFiles Project: faVsco.js, menu
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/.30.01Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45421
|
NULL
|
NULL
|
NULL
|
|
45425
|
1625
|
57
|
2026-05-14T14:36:10.248407+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769370248_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:© PhoneNumber.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© PlaybackTheme.php© Playbook.php© PlaybookCategory.php© ImportActivityTypes.php© Playbook.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php X© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.php© Playlist.phpPlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging18© RateLimit.php© Region.php© Role.php© RoleChangeEvent.php© ScopeGroup.php© Session.php© SlackBot.php© SocialAccount.php© Stage.php© Task.php© Team.php© TeamAiContext.php© TeamDomain.php© TeamFeature.php© TeamSettings.php© TextRelay.php© Track.php© TranscriptionModel.php© TranscriptionModelLocale.php© TranscriptionProvider.php© User.php© UserSettings.php© Vocabulary.php© VocabularyPronunciation.php© VoiceAccess.php© VoiceConsentPrefix.phpD NotificationsD ObserversD PoliciesD ProvidersE.env© HubspotPaginationService.php15.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 VasilevDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php244445464748 Ct © HandleHubspotRateLimit.phptrait LogActivityTrait* Save an activity, updateCRM prospect objects**/т А6x1^* Create or update a task or event (depending on most applicable playbook)public function saveActivity(Activity $activity): Activity$playbook = $this->getPlaybookFromActivity($activity);if ($playbook === null) {throw new InvalidArgumentException( message: 'Please configure a Playbook first.T.223$this->logger->info('[Salesforce] Log activity', ['activity_id' => $activity->getIdO.'object_type' => $playbook->getActivityType(),1):$payload = ['OwnerId'=> $this->profile->getCrmProviderId(),'Description' => $this->getActivityDecorator(->generateDescription(Sactivity),]:$payload += Sthis->buildSubjectPayload($activity, $playbook);$payload += $this->buildProspectPayload(Sactivity);=$crmObjectType = $playbook->getActivityType( === Playbook::ACTIVITY_TYPE_TASK9 Antivitu CantinaDenvidor nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)100% <8• Thu 14 May 17:36:10* :Q=custom.log= laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217218219id = 33;220221222224id THEN '(own225— 226227228229230hubspat":231232233 V= 11512582;234W Windsurf Teams58:44UTF-8Ca 4 spaces...
|
NULL
|
-3200977138525391409
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:© PhoneNumber.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© PlaybackTheme.php© Playbook.php© PlaybookCategory.php© ImportActivityTypes.php© Playbook.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php X© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.php© Playlist.phpPlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging18© RateLimit.php© Region.php© Role.php© RoleChangeEvent.php© ScopeGroup.php© Session.php© SlackBot.php© SocialAccount.php© Stage.php© Task.php© Team.php© TeamAiContext.php© TeamDomain.php© TeamFeature.php© TeamSettings.php© TextRelay.php© Track.php© TranscriptionModel.php© TranscriptionModelLocale.php© TranscriptionProvider.php© User.php© UserSettings.php© Vocabulary.php© VocabularyPronunciation.php© VoiceAccess.php© VoiceConsentPrefix.phpD NotificationsD ObserversD PoliciesD ProvidersE.env© HubspotPaginationService.php15.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev27.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 VasilevDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php244445464748 Ct © HandleHubspotRateLimit.phptrait LogActivityTrait* Save an activity, updateCRM prospect objects**/т А6x1^* Create or update a task or event (depending on most applicable playbook)public function saveActivity(Activity $activity): Activity$playbook = $this->getPlaybookFromActivity($activity);if ($playbook === null) {throw new InvalidArgumentException( message: 'Please configure a Playbook first.T.223$this->logger->info('[Salesforce] Log activity', ['activity_id' => $activity->getIdO.'object_type' => $playbook->getActivityType(),1):$payload = ['OwnerId'=> $this->profile->getCrmProviderId(),'Description' => $this->getActivityDecorator(->generateDescription(Sactivity),]:$payload += Sthis->buildSubjectPayload($activity, $playbook);$payload += $this->buildProspectPayload(Sactivity);=$crmObjectType = $playbook->getActivityType( === Playbook::ACTIVITY_TYPE_TASK9 Antivitu CantinaDenvidor nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)100% <8• Thu 14 May 17:36:10* :Q=custom.log= laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217218219id = 33;220221222224id THEN '(own225— 226227228229230hubspat":231232233 V= 11512582;234W Windsurf Teams58:44UTF-8Ca 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45426
|
1625
|
58
|
2026-05-14T14:36:15.412009+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769375412_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% С8• Thu 14 May 17:36:15Q•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log→ JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php= laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php© LocalSearchinterface.phpE.env© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]C RemoteSearch.php© Service.phpv C Listeners© HubspotPaginationService.php15.04.26 Vasilev15.04.26 Vasilev2448A6 X1A© ConvertLeadActivities.php© PurgeLookupCache.php15.04.26 Vasilev20.04.26 Vasilev› D Metadata© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activityif ($playbook === null) {throw new InvalidArgumentException( message:'Please configure a Playbook first.':216A console (EU]> C Migration15.04.26 Vasilevv 0 Pipedrive15.04.26 Vasilev> D OpportunitySyncStrategy27.04.26 Vasilev27.04.26 Vasilev> D ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev© Client.php© FieldDefinitions.php27.04.26 Vasilev© PipedriveApiClient.php15.04.26 Vasilev• PipedriveApiException.php23.04.26 Vasilev© Service.php20.04.26 Vasilev© TokenStorage.php15.04.26 Vasilevv D Salesforce15.04.26 Vasilev› [ Fields23.04.26 Vasilev• OpportunityMatcher23.04.26 Vasilev23.04.26 Vasilev• OpportunitySyncStrategy> D ProspectSearchStrategy23.04.26 Vasilevv • ServiceTraits23.04.26 VasilevBatchSyncTrait.php23.04.26 Vasilev+ FollowupActivityTrait.php23.04.26 VasilevT LogActivityTrait.php27.04.26 Vasilev+ RecordManipulationsTrait./27.04.26 VasilevT SyncFieldsTrait.php27.04.26 Vasilev@ Client nhn27.04.26 Vasilev $this-›logger-›info('[Salesforce] Log activity', ['activity_id'=> Sactivity->getId,'object_type' => $playbook->getActivityType().1):$payload = ['OwnerId'=> $this->profile->getCrmProviderId(),'Description' => $this->getActivityDecorator()->generateDescription($activity),*226J:$payload += Sthis->buildSubjectPayload(Sactivity, $playbook);$payload += $this->buildProspectPayload($activity);$crm0bjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK? Field::0BJECT_TASK: Field: :0BJECT_EVENT;DGo jiminny084143 ×4 л215•m_layout_id =2,1661,66799,66217218219id = 33;220221222223224id THEN '(own225227228229230hubspot':-231232233 V= 11512582;234$this->logger->info('[Salesforce] Log activity. Payload Prepared', ['activity_id' => $activity->getIdO.'object_type' => $playbook->getActivityType(),'crm_object_type' => $crm0bjectType,Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams58:44UTF-8Ca 4 spaces...
|
NULL
|
2988468458530886142
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% С8• Thu 14 May 17:36:15Q•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log→ JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php= laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php© LocalSearchinterface.phpE.env© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]C RemoteSearch.php© Service.phpv C Listeners© HubspotPaginationService.php15.04.26 Vasilev15.04.26 Vasilev2448A6 X1A© ConvertLeadActivities.php© PurgeLookupCache.php15.04.26 Vasilev20.04.26 Vasilev› D Metadata© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activityif ($playbook === null) {throw new InvalidArgumentException( message:'Please configure a Playbook first.':216A console (EU]> C Migration15.04.26 Vasilevv 0 Pipedrive15.04.26 Vasilev> D OpportunitySyncStrategy27.04.26 Vasilev27.04.26 Vasilev> D ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev© Client.php© FieldDefinitions.php27.04.26 Vasilev© PipedriveApiClient.php15.04.26 Vasilev• PipedriveApiException.php23.04.26 Vasilev© Service.php20.04.26 Vasilev© TokenStorage.php15.04.26 Vasilevv D Salesforce15.04.26 Vasilev› [ Fields23.04.26 Vasilev• OpportunityMatcher23.04.26 Vasilev23.04.26 Vasilev• OpportunitySyncStrategy> D ProspectSearchStrategy23.04.26 Vasilevv • ServiceTraits23.04.26 VasilevBatchSyncTrait.php23.04.26 Vasilev+ FollowupActivityTrait.php23.04.26 VasilevT LogActivityTrait.php27.04.26 Vasilev+ RecordManipulationsTrait./27.04.26 VasilevT SyncFieldsTrait.php27.04.26 Vasilev@ Client nhn27.04.26 Vasilev $this-›logger-›info('[Salesforce] Log activity', ['activity_id'=> Sactivity->getId,'object_type' => $playbook->getActivityType().1):$payload = ['OwnerId'=> $this->profile->getCrmProviderId(),'Description' => $this->getActivityDecorator()->generateDescription($activity),*226J:$payload += Sthis->buildSubjectPayload(Sactivity, $playbook);$payload += $this->buildProspectPayload($activity);$crm0bjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK? Field::0BJECT_TASK: Field: :0BJECT_EVENT;DGo jiminny084143 ×4 л215•m_layout_id =2,1661,66799,66217218219id = 33;220221222223224id THEN '(own225227228229230hubspot':-231232233 V= 11512582;234$this->logger->info('[Salesforce] Log activity. Payload Prepared', ['activity_id' => $activity->getIdO.'object_type' => $playbook->getActivityType(),'crm_object_type' => $crm0bjectType,Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams58:44UTF-8Ca 4 spaces...
|
45425
|
NULL
|
NULL
|
NULL
|
|
45427
|
1626
|
35
|
2026-05-14T14:36:15.429820+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769375429_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5840353787124240911
|
-7195274138713911831
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/.30.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45428
|
1625
|
59
|
2026-05-14T14:36:18.050018+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769378050_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv100% <8• Thu 14 May 17:36:17* :QProject v© UserAutomatedReportsController.php:=custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpD JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php• LocalSearchlnterface.phpC RemoteSearch.php© Service.phpv D ListenersE.env© HubspotPaginationService.php15.04.26 Vasilev15.04.26 VasilevDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]A console (EU]© ConvertLeadActivities.php© PurgeLookupCache.php› D Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev244878798081828384© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activity1);A6 X1A$this->saveCrm0bject($crm0bjectType, Sactivity, Spayload);return $activity:id = 33;© Client.php1 usage© FieldDefinitions.php23.04.26 Vasilev© PipedriveApiClient.php23.04.26 Vasilev• PipedriveApiException.php15.04.26 Vasilev© Service.php21.04.26 Vasilev© TokenStorage.php21.04.26Vasilevv D Salesforce21.04.26 Vasilev• Fields21.04.26 Vasilev• OpportunityMatcher21.04.26 Vasilev• OpportunitySyncStrategy21.04.26 VasilevD ProspectSearchStrategy21.04.26 Vasilevv • ServiceTraits21.04.26 VasilevT BatchSyncTrait.php23.04.26 Vasilev private function puildSubjectPayload(Activity $activity, Playbook $playbook): arrayif ($this-›matchesCrmType(Sactivity->getCategory)) {/*****/return [DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217218219220221222223— 224225226* If the activity playbook category name (i.e. activity type) matches a knowna.227* in the CRM, then we set the activity type in the payload.228* Some activity types may be locally created. They shouldn't be sent to Salesfo.229230231232233 V234id THEN(ownhubspat":* The resulting payload attribute will look like:'Type=> 'Cold Calz'= 11512582;+ FollowupActivityTrait.php23.04.26 VasilevT LogActivityTrait.php15.04.26 Vasilev+ RecordManipulationsTrait./20.04.26 Vasilev/** ActivityFieldID => ActivityType */Splaybook->getActivityField()->getCrmProviderId() => Sactivity->getCategoryf'Subject' => $this->getActivityDecorator()->generateTitle(Sactivity),T SyncFieldsTrait.php15.04.26 Vasilev]:@ Client nhn23.04.26 VasilevWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams85:22UTF-8Co 4 spaces...
|
NULL
|
8250304127479859936
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv100% <8• Thu 14 May 17:36:17* :QProject v© UserAutomatedReportsController.php:=custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpD JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php• LocalSearchlnterface.phpC RemoteSearch.php© Service.phpv D ListenersE.env© HubspotPaginationService.php15.04.26 Vasilev15.04.26 VasilevDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]A console (EU]© ConvertLeadActivities.php© PurgeLookupCache.php› D Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev244878798081828384© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activity1);A6 X1A$this->saveCrm0bject($crm0bjectType, Sactivity, Spayload);return $activity:id = 33;© Client.php1 usage© FieldDefinitions.php23.04.26 Vasilev© PipedriveApiClient.php23.04.26 Vasilev• PipedriveApiException.php15.04.26 Vasilev© Service.php21.04.26 Vasilev© TokenStorage.php21.04.26Vasilevv D Salesforce21.04.26 Vasilev• Fields21.04.26 Vasilev• OpportunityMatcher21.04.26 Vasilev• OpportunitySyncStrategy21.04.26 VasilevD ProspectSearchStrategy21.04.26 Vasilevv • ServiceTraits21.04.26 VasilevT BatchSyncTrait.php23.04.26 Vasilev private function puildSubjectPayload(Activity $activity, Playbook $playbook): arrayif ($this-›matchesCrmType(Sactivity->getCategory)) {/*****/return [DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217218219220221222223— 224225226* If the activity playbook category name (i.e. activity type) matches a knowna.227* in the CRM, then we set the activity type in the payload.228* Some activity types may be locally created. They shouldn't be sent to Salesfo.229230231232233 V234id THEN(ownhubspat":* The resulting payload attribute will look like:'Type=> 'Cold Calz'= 11512582;+ FollowupActivityTrait.php23.04.26 VasilevT LogActivityTrait.php15.04.26 Vasilev+ RecordManipulationsTrait./20.04.26 Vasilev/** ActivityFieldID => ActivityType */Splaybook->getActivityField()->getCrmProviderId() => Sactivity->getCategoryf'Subject' => $this->getActivityDecorator()->generateTitle(Sactivity),T SyncFieldsTrait.php15.04.26 Vasilev]:@ Client nhn23.04.26 VasilevWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams85:22UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45429
|
1625
|
60
|
2026-05-14T14:36:24.084001+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769384084_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7759925082980464890
|
-7195274138718106135
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% CFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv8• Thu 14 May 17:36:23* :QProject v© UserAutomatedReportsController.php:=custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpD JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php XSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php© LocalSearchinterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]C RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.phpA console (EU]15.04.26 Vasilev24trait LogActivityTraitv D ListenersA6 X1^DGojiminny v23.04.26 Vasilev85© ConvertLeadActivities.phpprivate function buildSubjectPayload(Activity $activity, Playbook Splaybook): array21.04.40vasllev79туpe-/cucu cuce084143 ×4 л— 215© PurgeLookupCache.php21.04.26 Vasilev95•m_layout_id =› D Metadata23.04.26 Vasilev96return [2162,1661,66799,66> C Migration21723.04.26 Vasilev97/** ActivityFieldID => ActivityType */v 0 Pipedrive15.04.26 Vasilev98$playbook->getActivityField(->getCrmProviderId( => Sactivity->getCategory®218> D OpportunitySyncStrategy20.04.26 Vasilev99219id = 33;'Subject' => Sthis->getActivityDecorator()->generateTitle(Sactivity),220> ProspectSearchStrategy15.04.26 Vasilev1001;© ApiFields.php22123.04.26 Vasilev101222© Client.php15.04.26 Vasilev102223© FieldDefinitions.php23.04.26 Vasilev103/**— 224|id THEN '(own© PipedriveApiClient.php23.04.26 Vasilev104* Building of the title seems very inconsistent• PipedriveApiException.php22523.04.26 Vasilev105* Subject here will be something like:© Service.php23.04.26 Vasilev106*"Local activity type: Activity Title"226© TokenStorage.php22723.04.26 Vasilev107*/v D Salesforce23.04.26 Vasilev108— 228$subject = Sactivity->getCategory(->getNameO:• Fields22923.04.26 Vasilev109if ($activity->getTitle() !== null) {230• OpportunityMatcher23.04.26 Vasilev110$subject .= ': ' . $activity->getTitle®:hubspot':-• OpportunitySyncStrategy15.04.26 Vasilev111231D ProspectSearchStrategy15.04.26 Vasilev112232v • ServiceTraits233 V23.04.26 Vasilev113return ['Subject' => $subject]:= 11512582;T BatchSyncTrait.php23423.04.26 Vasilev114+ FollowupActivityTrait.php23.04.26 Vasilev115T LogActivityTrait.php1 usage111+ RecordManipulationsTrait.f23.04.26 Vasilev116private function buildProspectPayload(Activity $activity): arrayf.T SyncFieldsTrait.php15.04.26 Vasilev172@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams85:22UTF-8Ca 4 spaces...
|
45428
|
NULL
|
NULL
|
NULL
|
|
45430
|
1625
|
61
|
2026-05-14T14:36:27.095100+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769387095_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
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.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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":"6","depth":4,"bounds":{"left":0.78194445,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.80277777,"top":0.3122222,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"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\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"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":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"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}]...
|
9077963656576291944
|
5299220392384270933
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45431
|
1626
|
36
|
2026-05-14T14:36:27.317044+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769387317_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
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.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"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":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"bounds":{"left":0.43716756,"top":0.15722266,"width":0.31848404,"height":0.8427774},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"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":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.28224733,"top":1.0,"width":0.024268618,"height":-0.04788506},"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}]...
|
9077963656576291944
|
5299220392384270933
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
45427
|
NULL
|
NULL
|
NULL
|
|
45432
|
1625
|
62
|
2026-05-14T14:36:31.502497+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769391502_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1160075880258684949
|
-8349599981661484599
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest100% (C478• Thu 14 May 17:36:31QProject v:AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpUserAutomatedReportsController.phpcustom.log• JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.phpProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php X© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.php© DecorateActivity.php18© LocalSearch.php© LocalSearchinterface.phpC RemoteSearch.php© Service.php© PlainTextDecorateActivity.phpE.env© HubspotPaginationService.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.stagingDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHandleHubspotRateLimit.phpv D Listeners15.04.26 Vasilev23.04.26 Vasilev2484trait LogActivityTraitA6 X1 л© ConvertLeadActivities.php© PurgeLookupCache.php1 usageprivatefunction buildSubjectPayload(Activity $activity, Playbook $playbook): array› D Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php• PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> D Fields• OpportunityMatcher• OpportunitySyncStrategyD ProspectSearchStrategyv • ServiceTraitsBatchSyncTrait.php+ FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.fT SyncFieldsTrait.php23.04.26 Vasilev23.04.26 Vasilev15.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26 Vasilev21.04.26Vasilev21.04.26 Vasilev21.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev15.04.26 Vasilev20.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev15.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev85868788F if (Sthis-›matchesCrmType(Sactivity->getCategory())) €/*** If the activity playbook category name (i.e. activity type) matches a known ac 220* in the CRM, then we set the activity type in the payload.* Some activity types may be locally created. They shouldn't be sent to Salesfor 222** The resulting payload attribute will look like:'Type' => 'Cold Call'*/return [/** ActivityFieldID => ActivityType */$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory(229'Subject' => $this->getActivityDecorator()->generateTitle(Sactivity),/****/* Building of the title seems very inconsistent* Subject here will be something like:"Local activity type: Activity Title"111Ssubiect = Sactivity->getCategory()->getNameO:l@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DSjiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66217— 218219id = 33;221223224id THEN(own225226227228230hubspot':231232233 V= 11512582;234W Windsurf Teams87:24UTF-8Co 4 spaces...
|
45430
|
NULL
|
NULL
|
NULL
|
|
45433
|
1625
|
63
|
2026-05-14T14:36:34.119078+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769394119_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
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.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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":"6","depth":4,"bounds":{"left":0.78194445,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.80277777,"top":0.3122222,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"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\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"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":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"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}]...
|
9077963656576291944
|
5299220392384270933
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45434
|
1626
|
37
|
2026-05-14T14:36:31.502458+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769391502_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1160075880258684949
|
-8349599981661484599
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterday at 13.3Yesterday at 13:36Yesterday at 13:36Yesterday at 13.30Yesterdav at 13:35Yesterday at 13:34Yesterday at 13:34Yesterdav at 13:3kYesterday at 13:33Yesterday at 13.32Yesterdav at 13:31resterday at 15:30Yesterdav at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28Yecterdav at 12:28resterday at 13-21Yesterdav at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13:25Voctorday at 12:01Yesterday at 13.23Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:19Yesterdav at 13:18Yesterday at 13-1/Yesterdav at 13:17Yesterday at 13:16Yesterday at 13:15Yecterdav at 12:15Yesterday at 13.14Yecterdav at 13:10Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:11Yesterday at 13.10Yecterdav at 12:10Yesterdav at 13:09Yesterdav at 13:08v Size23 KB9 KB13 Kb16 KE17 KB29 KB16 K:12 KB9 KEIKBn8 KBI37 KB10 KB7 KE8 KB9 KB8 KB172 KR14 KB13 KB9 KE18 KB12 KB10 KBI16 KB6 KB6KB12 KE23 KB8 KB16 K:11 KB20 KP34 KB10 KB7 K:5 KB11 KB26 KBMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movieMDSG-A movieMP2G-4 movieMPEG-4 movieMPEG"4 movieMPEG-4 movieMoEeh movioMPEG-4 movieMPEG-1 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEG.A movicMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEC A movidMPEG-4 movieMDEG-A movieMPEG-4 movieMPEG-4 movieMDEC.A moviaMPEG-4 movie111 KC102 KB88 KBMDEG-A moviaMPEG-4 movie59 KB1MPEG-A movie98 KB MPEG-4 movie97 KB66 K3MPEG-4 movieMDEG.A movioAAKP93 KB/8 KBMPEG-4 movie50 KгMDEG-A movid58 KBMPEG-4 movie27 KB7KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.45 GB availabold• Inu 14 May 1/•30.3)Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 г+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45435
|
1625
|
64
|
2026-05-14T14:36:34.630743+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769394630_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vlhl19HandleHubspotRateLimitTest100% (C478• Thu 14 May 17:36:34* :QProject v© UserAutomatedReportsController.php:= custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php→ JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php XSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php© LocalSearchinterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php4 console [PROD]C RemoteSearch.php© Service.php© HubspotPaginationService.phpHandleHubspotRateLimit.phpA console (EU]15.04.26 Vasilev24trait LogActivityTraitv C ListenersA6 X1 ^DGdjiminny v23.04.26 Vasilev84© ConvertLeadActivities.php084143 ×4 л1 usage— 215|© PurgeLookupCache.php23.04.26 Vasilevprivatefunction buildSubjectPayload(Activity $activity, Playbook $playbook): array› D Metadata85•m_layout_id =23.04.26 Vasilev862162,1661,66799,66> C Migration15.04.26 Vasilev87if217($this->matchesCrmType(Sactivity->getCategoryO)) {v 0 Pipedrive21.04.26 Vasilev/**218> D OpportunitySyncStrategy8821921.04.26 Vasilev* If the activity playbook category name (i.e. activity type) matches a known al 220> D ProspectSearchStrategy89id = 33;21.04.26 Vasilev90* in the CRM, then we set the activity type in the payload.© ApiFields.php22121.04.26 Vasilev91* Some activity types may be locally created. They shouldn't be sent to Salesfor 222© Client.php21.04.26Vasilev© FieldDefinitions.php92*22321.04.26 Vasilev93* The resulting payload attribute will look like:id THEN© PipedriveApiClient.php21.04.26 Vasilev94*'Type"=> 'Cold Call'224(own• PipedriveApiException.php21.04.26 Vasilev95*/225226© Service.php23.04.26 Vasilev96return [© TokenStorage.php23.04.26 Vasilev97/** ActivityFieldID => ActivityType */227228v D Salesforce15.04.26 Vasilev98• FieldsSplaybook->getActivityField()->getCrmProviderId() => Sactivity->getCategoryO22920.04.26 Vasilev• OpportunityMatcher99'Subject' => $this->getActivityDecorator()->generateTitle|(Sactivity),230hubspot':15.04.26 Vasilev100• OpportunitySyncStrategy1:23123.04.26 Vasilev101D ProspectSearchStrategy23215.04.26 Vasilev102v • ServiceTraits233 V= 11512582;BatchSyncTrait.php23.04.26 Vasilev103/**23423.04.26 Vasilev104* Building of the title seems very inconsistent+ FollowupActivityTrait.php23.04.26 Vasilev105* Subject here will be something like:T LogActivityTrait.php23.04.26 Vasilev106*"Local activity type: Activity Title"111+ RecordManipulationsTrait.f23.04.26 Vasilev107*/T SyncFieldsTrait.php23.04.26 Vasilev108$subject = Sactivity->getCategory(->getNameO:@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams99:74 (13 chars)UTF-8Co 4 spaces...
|
NULL
|
-6614538822017752566
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vlhl19HandleHubspotRateLimitTest100% (C478• Thu 14 May 17:36:34* :QProject v© UserAutomatedReportsController.php:= custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php→ JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php XSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php© LocalSearchinterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php4 console [PROD]C RemoteSearch.php© Service.php© HubspotPaginationService.phpHandleHubspotRateLimit.phpA console (EU]15.04.26 Vasilev24trait LogActivityTraitv C ListenersA6 X1 ^DGdjiminny v23.04.26 Vasilev84© ConvertLeadActivities.php084143 ×4 л1 usage— 215|© PurgeLookupCache.php23.04.26 Vasilevprivatefunction buildSubjectPayload(Activity $activity, Playbook $playbook): array› D Metadata85•m_layout_id =23.04.26 Vasilev862162,1661,66799,66> C Migration15.04.26 Vasilev87if217($this->matchesCrmType(Sactivity->getCategoryO)) {v 0 Pipedrive21.04.26 Vasilev/**218> D OpportunitySyncStrategy8821921.04.26 Vasilev* If the activity playbook category name (i.e. activity type) matches a known al 220> D ProspectSearchStrategy89id = 33;21.04.26 Vasilev90* in the CRM, then we set the activity type in the payload.© ApiFields.php22121.04.26 Vasilev91* Some activity types may be locally created. They shouldn't be sent to Salesfor 222© Client.php21.04.26Vasilev© FieldDefinitions.php92*22321.04.26 Vasilev93* The resulting payload attribute will look like:id THEN© PipedriveApiClient.php21.04.26 Vasilev94*'Type"=> 'Cold Call'224(own• PipedriveApiException.php21.04.26 Vasilev95*/225226© Service.php23.04.26 Vasilev96return [© TokenStorage.php23.04.26 Vasilev97/** ActivityFieldID => ActivityType */227228v D Salesforce15.04.26 Vasilev98• FieldsSplaybook->getActivityField()->getCrmProviderId() => Sactivity->getCategoryO22920.04.26 Vasilev• OpportunityMatcher99'Subject' => $this->getActivityDecorator()->generateTitle|(Sactivity),230hubspot':15.04.26 Vasilev100• OpportunitySyncStrategy1:23123.04.26 Vasilev101D ProspectSearchStrategy23215.04.26 Vasilev102v • ServiceTraits233 V= 11512582;BatchSyncTrait.php23.04.26 Vasilev103/**23423.04.26 Vasilev104* Building of the title seems very inconsistent+ FollowupActivityTrait.php23.04.26 Vasilev105* Subject here will be something like:T LogActivityTrait.php23.04.26 Vasilev106*"Local activity type: Activity Title"111+ RecordManipulationsTrait.f23.04.26 Vasilev107*/T SyncFieldsTrait.php23.04.26 Vasilev108$subject = Sactivity->getCategory(->getNameO:@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams99:74 (13 chars)UTF-8Co 4 spaces...
|
45433
|
NULL
|
NULL
|
NULL
|
|
45436
|
1626
|
38
|
2026-05-14T14:36:35.291710+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769395291_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterday at 13.3Yesterday at 13:36Yesterday at 13:36Yesterday at 13.30Yesterdav at 13:35Yesterday at 13:34Yesterday at 13:34Yesterdav at 13:3kYesterday at 13:33Yesterday at 13.32Yesterdav at 13:31resterday at 15:30Yesterdav at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28Yecterdav at 12:28resterday at 13-21Yesterdav at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13:25Voctorday at 12:01Yesterday at 13.23Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:19Yesterdav at 13:18Yesterday at 13-1/Yesterdav at 13:17Yesterday at 13:16Yesterday at 13:15Yecterdav at 12:15Yesterday at 13.14Yecterdav at 13:10Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:11Yesterday at 13.10Yecterdav at 12:10Yesterdav at 13:09Yesterdav at 13:08v Size23 KB9 KB13 Kb16 KE17 KB29 KB16 K:12 KB9 KEIKBn8 KBI37 KB10 KB7 KE8 KB9 KB8 KB172 KR14 KB13 KB9 KE18 KB12 KB10 KBI16 KB6 KB6KB12 KE23 KB8 KB16 K:11 KB20 KP34 KB10 KB7 K:5 KB11 KB26 KBMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movieMDSG-A movieMP2G-4 movieMPEG-4 movieMPEG"4 movieMPEG-4 movieMoEeh movioMPEG-4 movieMPEG-1 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEG.A movicMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEC A movidMPEG-4 movieMDEG-A movieMPEG-4 movieMPEG-4 movieMDEC.A moviaMPEG-4 movie111 KC102 KB88 KBMDEG-A moviaMPEG-4 movie59 KB1MPEG-A movie98 KB MPEG-4 movie97 KB66 K3MPEG-4 movieMDEG.A movioAAKP93 KB/8 KBMPEG-4 movie50 KгMDEG-A movid58 KBMPEG-4 movie27 KB7KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.45 GB availabold• Inu 14 May 1/•30.32Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 г+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-5050054199516888792
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterday at 13.3Yesterday at 13:36Yesterday at 13:36Yesterday at 13.30Yesterdav at 13:35Yesterday at 13:34Yesterday at 13:34Yesterdav at 13:3kYesterday at 13:33Yesterday at 13.32Yesterdav at 13:31resterday at 15:30Yesterdav at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28Yecterdav at 12:28resterday at 13-21Yesterdav at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13:25Voctorday at 12:01Yesterday at 13.23Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:19Yesterdav at 13:18Yesterday at 13-1/Yesterdav at 13:17Yesterday at 13:16Yesterday at 13:15Yecterdav at 12:15Yesterday at 13.14Yecterdav at 13:10Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:11Yesterday at 13.10Yecterdav at 12:10Yesterdav at 13:09Yesterdav at 13:08v Size23 KB9 KB13 Kb16 KE17 KB29 KB16 K:12 KB9 KEIKBn8 KBI37 KB10 KB7 KE8 KB9 KB8 KB172 KR14 KB13 KB9 KE18 KB12 KB10 KBI16 KB6 KB6KB12 KE23 KB8 KB16 K:11 KB20 KP34 KB10 KB7 K:5 KB11 KB26 KBMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movieMDSG-A movieMP2G-4 movieMPEG-4 movieMPEG"4 movieMPEG-4 movieMoEeh movioMPEG-4 movieMPEG-1 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEG.A movicMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEC A movidMPEG-4 movieMDEG-A movieMPEG-4 movieMPEG-4 movieMDEC.A moviaMPEG-4 movie111 KC102 KB88 KBMDEG-A moviaMPEG-4 movie59 KB1MPEG-A movie98 KB MPEG-4 movie97 KB66 K3MPEG-4 movieMDEG.A movioAAKP93 KB/8 KBMPEG-4 movie50 KгMDEG-A movid58 KBMPEG-4 movie27 KB7KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.45 GB availabold• Inu 14 May 1/•30.32Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 г+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45434
|
NULL
|
NULL
|
NULL
|
|
45437
|
1626
|
39
|
2026-05-14T14:36:40.307092+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769400307_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"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":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"bounds":{"left":0.43783244,"top":0.0,"width":0.31848404,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"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":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.28224733,"top":1.0,"width":0.024268618,"height":-0.04788506},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5226631486218845380
|
5299220392384270933
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45438
|
1625
|
65
|
2026-05-14T14:36:38.617343+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769398617_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% СFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest8• Thu 14 May 17:36:38* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:= custom.log• JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php XSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php• LocalSearchlnterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]C RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.phpA console (EU]15.04.26 Vasilev24trait LogActivityTraitv C Listeners46 м1 ^Gojiminny v23.04.26 Vasilev85© ConvertLeadActivities.phpprivate function buildSubjectPayload(Activity $activity, Playbook Splaybook): array084143 ×4 л© PurgeLookupCache.php21.04.26 Vasilev89* If the activity playbook category name (i.e. activity type)matches a known ac215•m_layout_id =21.04.26 Vasilev› D Metadata90* in the CRM, then we set the activity type in the payload.2162,1661,66799,66> C Migration21.04.26 Vasilev91* Some activity types may be locally created. They shouldn't besent toSalesfor 217v 0 Pipedrive21.04.26 Vasilev92*218> D OpportunitySyncStrategy21.04.26 Vasilev93* The resulting payload attribute will look like:219id = 33;> D ProspectSearchStrategy21.04.26 Vasilev94'Type"='Cold Call'220© ApiFields.php21.04.26 Vasilev95*/22123.04.26 Vasilev© Client.php96return [22223.04.26 Vasilev© FieldDefinitions.php97/** ActivityFieldID => ActivityType */223© PipedriveApiClient.php15.04.26 Vasilev98$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory(J224id THEN '(own• PipedriveApiException.php20.04.26 Vasilev99'Subject'=> Sthis->getActivityDecorator(->generateTitle(Sactivity),225© Service.php15.04.26 Vasilev10022623.04.26 Vasilev© TokenStorage.php101227v D Salesforce15.04.26 Vasilev102228• Fields23.04.26 Vasilev103/**22923.04.26 Vasilev• OpportunityMatcher104* Building of the title seems very inconsistent230hubspot':-23.04.26 Vasilev• OpportunitySyncStrategy105*Subject here will be something like:231D ProspectSearchStrategy23.04.26 Vasilev106"Local activity type: Activity Title"232v • ServiceTraits23.04.26 Vasilev107*/233 V= 11512582;T BatchSyncTrait.php23.04.26 Vasilev108$subject = Sactivity->getCategory()->getName():234+ FollowupActivityTrait.php23.04.26 Vasilev109if ($activity->getTitle() !== null) {T LogActivityTrait.php23.04.26 Vasilev110$subject .= ': ' . $activity->getTitle();+ RecordManipulationsTrait./15.04.26 Vasilev111T SyncFieldsTrait.php15.04.26 Vasilev112@ Client nhn23.04.26 Vasilev113return ['Subject'=> $subject]:Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams104:52 (40 chars)UTF-8Ca 4 spaces...
|
NULL
|
6840848990049322544
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% СFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest8• Thu 14 May 17:36:38* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:= custom.log• JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php XSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] X18© LocalSearch.php• LocalSearchlnterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]C RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.phpA console (EU]15.04.26 Vasilev24trait LogActivityTraitv C Listeners46 м1 ^Gojiminny v23.04.26 Vasilev85© ConvertLeadActivities.phpprivate function buildSubjectPayload(Activity $activity, Playbook Splaybook): array084143 ×4 л© PurgeLookupCache.php21.04.26 Vasilev89* If the activity playbook category name (i.e. activity type)matches a known ac215•m_layout_id =21.04.26 Vasilev› D Metadata90* in the CRM, then we set the activity type in the payload.2162,1661,66799,66> C Migration21.04.26 Vasilev91* Some activity types may be locally created. They shouldn't besent toSalesfor 217v 0 Pipedrive21.04.26 Vasilev92*218> D OpportunitySyncStrategy21.04.26 Vasilev93* The resulting payload attribute will look like:219id = 33;> D ProspectSearchStrategy21.04.26 Vasilev94'Type"='Cold Call'220© ApiFields.php21.04.26 Vasilev95*/22123.04.26 Vasilev© Client.php96return [22223.04.26 Vasilev© FieldDefinitions.php97/** ActivityFieldID => ActivityType */223© PipedriveApiClient.php15.04.26 Vasilev98$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory(J224id THEN '(own• PipedriveApiException.php20.04.26 Vasilev99'Subject'=> Sthis->getActivityDecorator(->generateTitle(Sactivity),225© Service.php15.04.26 Vasilev10022623.04.26 Vasilev© TokenStorage.php101227v D Salesforce15.04.26 Vasilev102228• Fields23.04.26 Vasilev103/**22923.04.26 Vasilev• OpportunityMatcher104* Building of the title seems very inconsistent230hubspot':-23.04.26 Vasilev• OpportunitySyncStrategy105*Subject here will be something like:231D ProspectSearchStrategy23.04.26 Vasilev106"Local activity type: Activity Title"232v • ServiceTraits23.04.26 Vasilev107*/233 V= 11512582;T BatchSyncTrait.php23.04.26 Vasilev108$subject = Sactivity->getCategory()->getName():234+ FollowupActivityTrait.php23.04.26 Vasilev109if ($activity->getTitle() !== null) {T LogActivityTrait.php23.04.26 Vasilev110$subject .= ': ' . $activity->getTitle();+ RecordManipulationsTrait./15.04.26 Vasilev111T SyncFieldsTrait.php15.04.26 Vasilev112@ Client nhn23.04.26 Vasilev113return ['Subject'=> $subject]:Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams104:52 (40 chars)UTF-8Ca 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45441
|
1626
|
41
|
2026-05-14T14:36:45.665681+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769405665_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterday at 13.3Yesterday at 13:36Yesterday at 13:36Yesterday at 13.30Yesterdav at 13:35Yesterday at 13:34Yesterday at 13:34Yesterdav at 13:3kYesterday at 13:33Yesterday at 13.32Yesterdav at 13:31resterday at 15:30Yesterdav at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28Yecterdav at 12:28resterday at 13-21Yesterdav at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13:25Voctorday at 12:01Yesterday at 13.23Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:19Yesterdav at 13:18Yesterday at 13-1/Yesterdav at 13:17Yesterday at 13:16Yesterday at 13:15Yecterdav at 12:15Yesterday at 13.14Yecterdav at 13:10Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:11Yesterday at 13.10Yecterdav at 12:10Yesterdav at 13:09Yesterdav at 13:08v Size23 KB9 KB13 Kb16 KE17 KB29 KB16 K:12 KB9 KEIKBn8 KBI37 KB10 KB7 KE8 KB9 KB8 KB172 KR14 KB13 KB9 KE18 KB12 KB10 KBI16 KB6 KB6KB12 KE23 KB8 KB16 K:11 KB20 KP34 KB10 KB7 K:5 KB11 KB26 KBMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movieMDSG-A movieMP2G-4 movieMPEG-4 movieMPEG"4 movieMPEG-4 movieMoEeh movioMPEG-4 movieMPEG-1 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEG.A movicMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEC A movidMPEG-4 movieMDEG-A movieMPEG-4 movieMPEG-4 movieMDEC.A moviaMPEG-4 movie111 KC102 KB88 KBMDEG-A moviaMPEG-4 movie59 KB1MPEG-A movie98 KB MPEG-4 movie97 KB66 K3MPEG-4 movieMDEG.A movioAAKP93 KB/8 KBMPEG-4 movie50 KгMDEG-A movid58 KBMPEG-4 movie27 KB7KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.46 GB availabld• Inu 14 Mау 1/•30.42Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 г+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
2098038158965143539
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterday at 13.3Yesterday at 13:36Yesterday at 13:36Yesterday at 13.30Yesterdav at 13:35Yesterday at 13:34Yesterday at 13:34Yesterdav at 13:3kYesterday at 13:33Yesterday at 13.32Yesterdav at 13:31resterday at 15:30Yesterdav at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28Yecterdav at 12:28resterday at 13-21Yesterdav at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13:25Voctorday at 12:01Yesterday at 13.23Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:19Yesterdav at 13:18Yesterday at 13-1/Yesterdav at 13:17Yesterday at 13:16Yesterday at 13:15Yecterdav at 12:15Yesterday at 13.14Yecterdav at 13:10Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:11Yesterday at 13.10Yecterdav at 12:10Yesterdav at 13:09Yesterdav at 13:08v Size23 KB9 KB13 Kb16 KE17 KB29 KB16 K:12 KB9 KEIKBn8 KBI37 KB10 KB7 KE8 KB9 KB8 KB172 KR14 KB13 KB9 KE18 KB12 KB10 KBI16 KB6 KB6KB12 KE23 KB8 KB16 K:11 KB20 KP34 KB10 KB7 K:5 KB11 KB26 KBMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movieMDSG-A movieMP2G-4 movieMPEG-4 movieMPEG"4 movieMPEG-4 movieMoEeh movioMPEG-4 movieMPEG-1 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEG.A movicMPEG-4 movieMPEG-4 movieMPEG-4 movieMPEG-4 movieMDEC A movidMPEG-4 movieMDEG-A movieMPEG-4 movieMPEG-4 movieMDEC.A moviaMPEG-4 movie111 KC102 KB88 KBMDEG-A moviaMPEG-4 movie59 KB1MPEG-A movie98 KB MPEG-4 movie97 KB66 K3MPEG-4 movieMDEG.A movioAAKP93 KB/8 KBMPEG-4 movie50 KгMDEG-A movid58 KBMPEG-4 movie27 KB7KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.46 GB availabld• Inu 14 Mау 1/•30.42Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 г+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45443
|
NULL
|
0
|
2026-05-14T14:36:49.452652+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769409452_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.46 GB availabld• Inu 14 Mау 1/•30.44Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
771622661116508809
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.46 GB availabld• Inu 14 Mау 1/•30.44Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45441
|
NULL
|
NULL
|
NULL
|
|
45444
|
1627
|
0
|
2026-05-14T14:37:08.071469+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769428071_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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":"6","depth":4,"bounds":{"left":0.78194445,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.80277777,"top":0.3122222,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"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\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8078283887967787415
|
7759357430035650948
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny...
|
45442
|
NULL
|
NULL
|
NULL
|
|
45445
|
1628
|
0
|
2026-05-14T14:37:08.052513+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769428052_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"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":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"bounds":{"left":0.43783244,"top":0.12210695,"width":0.31848404,"height":0.87789303},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3525368579526985675
|
7758231530128816516
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45446
|
1627
|
1
|
2026-05-14T14:37:09.443270+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769429443_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest100% <8• Thu 14 May 17:37:09* :QProject v© UserAutomatedReportsController.php:=custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpD JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php18© LocalSearch.php• LocalSearchlnterface.phpC RemoteSearch.php© Service.phpv D Listeners© PlainTextDecorateActivity.phpE.env© HubspotPaginationService.php15.04.26 Vasilev15.04.26 VasilevT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] XDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]A console (EU]© ConvertLeadActivities.php© PurgeLookupCache.php› D Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev244878798081828384© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activity1);A6 X1A$this->saveCrm0bject(ScrmObjectType, Sactivity, Spayload);return Sactivity:id = 33;© Client.php1 usage© FieldDefinitions.php23.04.26 Vasilev© PipedriveApiClient.php23.04.26 Vasilev• PipedriveApiException.php15.04.26 Vasilev© Service.php21.04.26 Vasilev© TokenStorage.php21.04.26 Vasilevv D Salesforce21.04.26 Vasilev• Fields21.04.26 Vasilev• OpportunityMatcher21.04.26Vasilev• OpportunitySyncStrategy21.04.26 VasilevD ProspectSearchStrategy21.04.26 Vasilevv • ServiceTraits21.04.26 Vasilev23.04.26 Vasilev private function buildSubjectPayload(Activity $activity, Playbook $playbook): arrayif ($this->matchesCrmType($activity->getCategory))) {/***DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217— 218219220221222223224225226* If the activity playbook category name (i.e. activity type) matches a knownac 227* in the CRM, then we set the activity type in the payload.228* Some activity types may be locally created. They shouldn't besent to Salesfor229230231232233 V234id THEN(ownhubspot":* The resulting payload attribute will look like:'Type"=> 'Cold Call'BatchSyncTrait.php*/return [= 11512582;+ FollowupActivityTrait.php23.04.26 VasilevT LogActivityTrait.php15.04.26 Vasilev+ RecordManipulationsTrait.f20.04.26 Vasilev/** ActivityFieldID => ActivityType */$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory€'Subject' => $this->getActivityDecorator()->generateTitle(Sactivity),T SyncFieldsTrait.php15.04.26 Vasilev]:@ Client nhn23.04.26 VasilevWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams87:34 (14 chars)UTF-8Co 4 spaces...
|
NULL
|
9062883269134786823
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest100% <8• Thu 14 May 17:37:09* :QProject v© UserAutomatedReportsController.php:=custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpD JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php18© LocalSearch.php• LocalSearchlnterface.phpC RemoteSearch.php© Service.phpv D Listeners© PlainTextDecorateActivity.phpE.env© HubspotPaginationService.php15.04.26 Vasilev15.04.26 VasilevT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] XDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]A console (EU]© ConvertLeadActivities.php© PurgeLookupCache.php› D Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev244878798081828384© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activity1);A6 X1A$this->saveCrm0bject(ScrmObjectType, Sactivity, Spayload);return Sactivity:id = 33;© Client.php1 usage© FieldDefinitions.php23.04.26 Vasilev© PipedriveApiClient.php23.04.26 Vasilev• PipedriveApiException.php15.04.26 Vasilev© Service.php21.04.26 Vasilev© TokenStorage.php21.04.26 Vasilevv D Salesforce21.04.26 Vasilev• Fields21.04.26 Vasilev• OpportunityMatcher21.04.26Vasilev• OpportunitySyncStrategy21.04.26 VasilevD ProspectSearchStrategy21.04.26 Vasilevv • ServiceTraits21.04.26 Vasilev23.04.26 Vasilev private function buildSubjectPayload(Activity $activity, Playbook $playbook): arrayif ($this->matchesCrmType($activity->getCategory))) {/***DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- 217— 218219220221222223224225226* If the activity playbook category name (i.e. activity type) matches a knownac 227* in the CRM, then we set the activity type in the payload.228* Some activity types may be locally created. They shouldn't besent to Salesfor229230231232233 V234id THEN(ownhubspot":* The resulting payload attribute will look like:'Type"=> 'Cold Call'BatchSyncTrait.php*/return [= 11512582;+ FollowupActivityTrait.php23.04.26 VasilevT LogActivityTrait.php15.04.26 Vasilev+ RecordManipulationsTrait.f20.04.26 Vasilev/** ActivityFieldID => ActivityType */$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory€'Subject' => $this->getActivityDecorator()->generateTitle(Sactivity),T SyncFieldsTrait.php15.04.26 Vasilev]:@ Client nhn23.04.26 VasilevWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams87:34 (14 chars)UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45447
|
1628
|
1
|
2026-05-14T14:37:09.426822+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769429426_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/•3/•00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-8335772991236113394
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/•3/•00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45445
|
NULL
|
NULL
|
NULL
|
|
45448
|
1627
|
2
|
2026-05-14T14:37:13.307193+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769433307_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest100% <8• Thu 14 May 17:37:12* :QProject v© UserAutomatedReportsController.php:=custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpD JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php18© LocalSearch.php• LocalSearchlnterface.phpC RemoteSearch.php© Service.phpv C Listeners© PlainTextDecorateActivity.phpE.env© HubspotPaginationService.php15.04.26 Vasilev15.04.26 VasilevT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] XDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]2448© ConvertLeadActivities.php© PurgeLookupCache.php© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activity1);A console (EU]A6 X1 ^› D Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev78798081828384$this->saveCrm0bject(ScrmObjectType, Sactivity, Spayload);return Sactivity:id = 33;© Client.php1 usage© FieldDefinitions.php23.04.26 Vasilev© PipedriveApiClient.php23.04.26 Vasilev• PipedriveApiException.php15.04.26 Vasilev© Service.php21.04.26 Vasilev© TokenStorage.php21.04.26 Vasilevv D Salesforce21.04.26 Vasilev• Fields21.04.26 Vasilev• OpportunityMatcher21.04.26Vasilev• OpportunitySyncStrategy21.04.26 VasilevD ProspectSearchStrategy21.04.26 Vasilevv • ServiceTraits21.04.26 Vasilev23.04.26 Vasilev private function buildSubjectPayload(Activity $activity, Playbook $playbook): arrayif ($this->matchesCrmType(Sactivity->getCategoryO)) 1/***DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- * If the activity playbook category name (i.e. activity type) matches a knownac227* in the CRM, then we set the activity type in the payload.228* Some activity types may be locally created. They shouldn't besent to Salesfor229230231232233 V234id THEN(ownhubspat":* The resulting payload attribute will look like:'Type"=> 'Cold Call'BatchSyncTrait.php+ FollowupActivityTrait.php23.04.26 VasilevFT LogActivityTrait.php15.04.26 Vasilev+ RecordManipulationsTrait.f20.04.26 Vasilev*/return [/** ActivityFieldID => ActivityType */$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory€'Supject'=> $this->getActivityDecorator()->generateTitle($activity),= 11512582;T SyncFieldsTrait.php15.04.26 Vasilev]:@ Client nhn23.04.26 VasilevWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams99:20UTF-8Co 4 spaces...
|
NULL
|
2765905447232264644
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest100% <8• Thu 14 May 17:37:12* :QProject v© UserAutomatedReportsController.php:=custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpD JobsPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php=laravel.logProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php xSF [jiminny@localhost]© DataClient.php© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpA HS_local [jiminny@localho:W© DecorateActivity.php18© LocalSearch.php• LocalSearchlnterface.phpC RemoteSearch.php© Service.phpv C Listeners© PlainTextDecorateActivity.phpE.env© HubspotPaginationService.php15.04.26 Vasilev15.04.26 VasilevT ActivityPlaybookTrait.php© CrmHelperRepository.phpAccountController.php=.env.staging4 console [QAI PROD] XDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php4 console [PROD]2448© ConvertLeadActivities.php© PurgeLookupCache.php© HandleHubspotRateLimit.phptrait LogActivityTraitpublic function saveActivity(Activity $activity): Activity1);A console (EU]A6 X1 ^› D Metadata> C Migrationv 0 Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php27.04.26 Vasilev27.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev23.04.26 Vasilev78798081828384$this->saveCrm0bject(ScrmObjectType, Sactivity, Spayload);return Sactivity:id = 33;© Client.php1 usage© FieldDefinitions.php23.04.26 Vasilev© PipedriveApiClient.php23.04.26 Vasilev• PipedriveApiException.php15.04.26 Vasilev© Service.php21.04.26 Vasilev© TokenStorage.php21.04.26 Vasilevv D Salesforce21.04.26 Vasilev• Fields21.04.26 Vasilev• OpportunityMatcher21.04.26Vasilev• OpportunitySyncStrategy21.04.26 VasilevD ProspectSearchStrategy21.04.26 Vasilevv • ServiceTraits21.04.26 Vasilev23.04.26 Vasilev private function buildSubjectPayload(Activity $activity, Playbook $playbook): arrayif ($this->matchesCrmType(Sactivity->getCategoryO)) 1/***DGo jiminny084143 ×4 ^— 215•m_layout_id =2162,1661,66799,66- * If the activity playbook category name (i.e. activity type) matches a knownac227* in the CRM, then we set the activity type in the payload.228* Some activity types may be locally created. They shouldn't besent to Salesfor229230231232233 V234id THEN(ownhubspat":* The resulting payload attribute will look like:'Type"=> 'Cold Call'BatchSyncTrait.php+ FollowupActivityTrait.php23.04.26 VasilevFT LogActivityTrait.php15.04.26 Vasilev+ RecordManipulationsTrait.f20.04.26 Vasilev*/return [/** ActivityFieldID => ActivityType */$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory€'Supject'=> $this->getActivityDecorator()->generateTitle($activity),= 11512582;T SyncFieldsTrait.php15.04.26 Vasilev]:@ Client nhn23.04.26 VasilevWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams99:20UTF-8Co 4 spaces...
|
45446
|
NULL
|
NULL
|
NULL
|
|
45449
|
1628
|
2
|
2026-05-14T14:37:13.307212+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769433307_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 Mаy 1/•3/:12Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-4061887643733524785
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:39resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterdav at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterdav at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yoctorday at 12:19Yesterday at 13.11Yecterdav at 12:10Yesterdav at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KB23 KBMPEG-4 movieMPEG-4 movieMPEG-4 movie13 KE16 KB17 KB29 K:MPE0"4 movie6 KB12 KBMDSG-A movieMP2G-4 movie7 KBI8 KB37 KB10 KВIMPEG-A movieMPEG"4 movieMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KR72 KB14 KBMPEG-1 movieMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KB16 KB6KBMDEG.A movicMPEG-4 movie6KE12 KBMPEG-4 movieMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMDECA movid11 KBMPEG-4 movie11 KPMDEG-A movie20 KB34 KB10 K:7 KB5 KB11 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KEMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBMPEG-4 movieMDEG.A movioGAKP44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 Mаy 1/•3/:12Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45450
|
1627
|
3
|
2026-05-14T14:37:17.748410+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769437748_m1.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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":"6","depth":4,"bounds":{"left":0.78194445,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.80277777,"top":0.3122222,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"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\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3525368579526985675
|
7758231530128816516
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}
Execute
Explain Plan
Browse Query History
View Parameters...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45451
|
1628
|
3
|
2026-05-14T14:37:17.748387+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769437748_m2.jpg...
|
PhpStorm
|
faVsco.js – LogActivityTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"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":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","depth":4,"bounds":{"left":0.43783244,"top":0.12210695,"width":0.31848404,"height":0.87789303},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Jiminny\\Events\\Activities\\Crm\\ActivityLeadConverted;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Services\\Crm\\Helpers\\CrmHelperRepository;\nuse Jiminny\\Services\\Crm\\Salesforce\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\n\ntrait LogActivityTrait\n{\n public ?Configuration $config;\n public ?Profile $profile;\n\n protected PayloadBuilder $payloadBuilder;\n protected LoggerInterface $logger;\n protected Dispatcher $eventDispatcher;\n protected CrmHelperRepository $crmHelperRepository;\n protected ?DecorateActivity $decorateActivity = null;\n\n abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;\n abstract public function matchesCrmType(PlaybookCategory $category): bool;\n\n abstract public function syncLead(string $crmId): ?Lead;\n abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;\n abstract public function createRecord(string $objectType, array $data): string;\n abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;\n\n /**\n * Save an activity, update CRM prospect objects\n *\n * Create or update a task or event (depending on most applicable playbook)\n */\n public function saveActivity(Activity $activity): Activity\n {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n throw new InvalidArgumentException('Please configure a Playbook first.');\n }\n\n $this->logger->info('[Salesforce] Log activity', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n ]);\n\n $payload = [\n 'OwnerId' => $this->profile->getCrmProviderId(),\n 'Description' => $this->getActivityDecorator()->generateDescription($activity),\n ];\n\n $payload += $this->buildSubjectPayload($activity, $playbook);\n $payload += $this->buildProspectPayload($activity);\n\n $crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK\n ? Field::OBJECT_TASK\n : Field::OBJECT_EVENT;\n\n $this->logger->info('[Salesforce] Log activity. Payload Prepared', [\n 'activity_id' => $activity->getId(),\n 'object_type' => $playbook->getActivityType(),\n 'crm_object_type' => $crmObjectType,\n 'payload' => $payload,\n ]);\n\n $this->saveCrmObject($crmObjectType, $activity, $payload);\n\n return $activity;\n }\n\n private function buildSubjectPayload(Activity $activity, Playbook $playbook): array\n {\n if ($this->matchesCrmType($activity->getCategory())) {\n /**\n * If the activity playbook category name (i.e. activity type) matches a known activity type that exists\n * in the CRM, then we set the activity type in the payload.\n * Some activity types may be locally created. They shouldn't be sent to Salesforce.\n *\n * The resulting payload attribute will look like:\n * `Type` => 'Cold Call'\n */\n return [\n /** ActivityFieldID => ActivityType */\n $playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),\n 'Subject' => $this->getActivityDecorator()->generateTitle($activity),\n ];\n }\n\n /**\n * Building of the title seems very inconsistent\n * Subject here will be something like:\n * \"Local activity type: Activity Title\"\n */\n $subject = $activity->getCategory()->getName();\n if ($activity->getTitle() !== null) {\n $subject .= ': ' . $activity->getTitle();\n }\n\n return ['Subject' => $subject];\n }\n\n private function buildProspectPayload(Activity $activity): array\n {\n $payload = [];\n\n if ($activity->getAccountId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'account_id' => $activity->getAccountId(),\n 'opportunity_id' => $activity->getOpportunityId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasLinkFeature = $this->crmHelperRepository->hasFeature(\n $activity,\n FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS\n );\n\n if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())\n || (! $hasLinkFeature && $activity->getOpportunityId())\n ) {\n $payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();\n } else {\n $payload['WhatId'] = $activity->getAccount()->getCrmProviderId();\n }\n }\n\n if ($activity->getContactId()) {\n $this->logger->info('[Salesforce] Activity has account id', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $payload['WhoId'] = $activity->getContact()->getCrmProviderId();\n } elseif ($activity->getLeadId()) {\n $this->logger->info('[Salesforce] Activity has lead id', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->getLeadId(),\n ]);\n\n /**\n * An activity may have:\n * a) a contact | account | opportunity (or combinations)\n * b) only a lead\n * c) none of the above\n *\n * An activity should not have a lead, and another prospect object at the same time.\n *\n * This logical flow should not happen naturally, but as a fallback: if an activity has both\n * a Lead and an Account, the account's WhatId takes precedence in the CRM payload.\n * The lead association will be detached locally, but the account WhatId is preserved in Salesforce.\n */\n $payload += $this->handleActivityLead($activity);\n }\n\n return $payload;\n }\n\n private function handleActivityLead(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Activity sync lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Sync lead to pull fresh data. Leads change often,\n * they may get 'converted', and we must handle the new state accordingly\n */\n try {\n $activityLead = $this->syncLead(\n $activity->getLead()->getCrmProviderId()\n );\n } catch (HttpNotFoundException) {\n $this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not found, maybe it's remotely deleted\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n if ($activityLead === null) {\n $this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [\n 'activity_id' => $activity->getId(),\n ]);\n\n /**\n * Lead is not loaded, for some reason. It may be remotely found, but locally deleted.\n * Or an important Lead property may be missing, and it cannot be fully imported.\n */\n $this->resetLeadAssociation($activity);\n\n return [];\n }\n\n /**\n * The lead is not converted, directly use it as WhoId and move on.\n */\n if ($activityLead->getConvertedAt() === null) {\n return [\n 'WhoId' => $activity->getLead()->getCrmProviderId(),\n 'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.\n ];\n }\n\n /**\n * Task/Event payload properties for the CRM update\n */\n $whoId = null;\n $whatId = null;\n\n /**\n * Activity payload properties, that need to be updated.\n */\n $convertedOpportunityId = null;\n $convertedAccountId = null;\n $convertedContactId = null;\n $value = null;\n\n /**\n * If there is converted opportunity, it will update the activity stage.\n * If there isn't an opportunity, a converted account/contact cannot have a stage id,\n * so the activity shouldn't have a stage either.\n */\n $stageId = null;\n\n /**\n * The lead is definitely converted.\n * This means the lead may be associated with an opportunity, account, or contact.\n * The converted lead itself must be detached from the activity,\n * while the converted prospect objects must be associated instead,\n * since they now represent the state of the deal.\n */\n $activity->setLeadId(null);\n\n /**\n * CRM property \"WhatId\" can be either an Opportunity or an Account.\n * For us a converted Opportunity has higher priority than an Account.\n * An Opportunity carries more concrete data, including a reference to the Account record itself.\n */\n if ($activityLead->hasOpportunity()) {\n $convertedOpportunity = $activityLead->getOpportunity();\n\n $convertedOpportunityId = $convertedOpportunity->getId();\n $convertedAccountId = $convertedOpportunity->getAccountId();\n $stageId = $convertedOpportunity->getStageId();\n $value = $convertedOpportunity->getValue();\n\n $whatId = $convertedOpportunity->getCrmProviderId();\n }\n\n if ($activityLead->hasAccount()) {\n $convertedAccount = $activityLead->getAccount();\n\n $convertedAccountId ??= $convertedAccount->getId();\n\n $whatId ??= $convertedAccount->getCrmProviderId();\n }\n\n /**\n * WhoId can only be a Contact or a Lead.\n * Since we are detaching the Lead, it can only be a Contact for the current activity.\n * It does not conflict with any other object.\n */\n if ($activityLead->hasContact()) {\n $convertedContact = $activityLead->getContact();\n\n $convertedContactId = $convertedContact->getId();\n\n $whoId = $convertedContact->getCrmProviderId();\n }\n\n $activity->setOpportunityId($convertedOpportunityId);\n $activity->setAccountId($convertedAccountId);\n $activity->setContactId($convertedContactId);\n $activity->setStageId($stageId);\n $activity->setValue($value);\n\n $activity->save();\n\n $this->eventDispatcher->dispatch(\n new ActivityLeadConverted($activity, $activityLead)\n );\n\n $this->logger->info('[Salesforce] Lead is converted', [\n 'activity_id' => $activity->getId(),\n 'whoId' => $whoId,\n 'whatId' => $whatId,\n ]);\n\n return [\n 'WhoId' => $whoId,\n 'WhatId' => $whatId,\n ];\n }\n\n private function saveCrmObject(string $objectType, Activity $activity, array $payload): void\n {\n /**\n * Tasks and events have different payload structures,\n * so we need to consider the type of the object we are sending to the CRM\n */\n $specificPayload = $objectType === Field::OBJECT_TASK\n ? $this->payloadBuilder->buildTaskPayload($activity)\n : $this->payloadBuilder->buildEventPayload($activity);\n\n $payload = array_merge($payload, $specificPayload);\n\n if ($activity->hasCrmProviderId()) {\n $this->logger->info('[Salesforce] Updating crm object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n try {\n $this->updateCrmActivity($objectType, $activity, $payload);\n } catch (HttpNotFoundException) {\n $this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $activity->setCrmProviderId(null);\n $activity->save();\n }\n\n return;\n }\n\n $this->logger->info('[Salesforce] Creating CRM object', [\n 'activity_id' => $activity->getId(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'object_type' => $objectType,\n ]);\n\n $crmProviderId = $this->createRecord($objectType, $payload);\n\n $activity->setCrmProviderId($crmProviderId);\n $activity->save();\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void\n {\n $sfActivity = $this->getRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n fields: ['Description', 'WhoId', 'WhatId']\n );\n\n if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {\n $this->logger->info('[Salesforce] Updating WhoId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n\n if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {\n $this->logger->info('[Salesforce] Updating WhatId', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n ]);\n } else {\n $payload['WhatId'] = null;\n }\n }\n\n $payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(\n $payload['Description'],\n $sfActivity['Description'] ?? null\n );\n\n $this->logger->info('[Salesforce] Updating CRM activity data', [\n 'objectType' => $objectType,\n 'activityId' => $activity->getUuid(),\n 'crmActivity' => $sfActivity,\n 'payload' => $payload,\n ]);\n\n $this->updateRecord(\n objectType: $objectType,\n objectId: $activity->getCrmProviderId(),\n data: $payload\n );\n }\n\n protected function getActivityDecorator(): DecorateActivity\n {\n if ($this->decorateActivity === null) {\n $this->decorateActivity = app(DecorateActivity::class);\n }\n\n return $this->decorateActivity;\n }\n\n private function resetLeadAssociation(Activity $activity): void\n {\n $activity->setStageId(null);\n $activity->setLeadId(null);\n $activity->save();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-2724287045098079756
|
7758231530128816516
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Salesforce\ServiceTraits;
use Illuminate\Events\Dispatcher;
use Jiminny\Events\Activities\Crm\ActivityLeadConverted;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Lead;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Services\Crm\Helpers\CrmHelperRepository;
use Jiminny\Services\Crm\Salesforce\DecorateActivity;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Psr\Log\LoggerInterface;
trait LogActivityTrait
{
public ?Configuration $config;
public ?Profile $profile;
protected PayloadBuilder $payloadBuilder;
protected LoggerInterface $logger;
protected Dispatcher $eventDispatcher;
protected CrmHelperRepository $crmHelperRepository;
protected ?DecorateActivity $decorateActivity = null;
abstract public function getPlaybookFromActivity(Activity $activity): ?Playbook;
abstract public function matchesCrmType(PlaybookCategory $category): bool;
abstract public function syncLead(string $crmId): ?Lead;
abstract public function getRecord(string $objectType, string $objectId, array $fields = []): array;
abstract public function createRecord(string $objectType, array $data): string;
abstract public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void;
/**
* Save an activity, update CRM prospect objects
*
* Create or update a task or event (depending on most applicable playbook)
*/
public function saveActivity(Activity $activity): Activity
{
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
throw new InvalidArgumentException('Please configure a Playbook first.');
}
$this->logger->info('[Salesforce] Log activity', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
]);
$payload = [
'OwnerId' => $this->profile->getCrmProviderId(),
'Description' => $this->getActivityDecorator()->generateDescription($activity),
];
$payload += $this->buildSubjectPayload($activity, $playbook);
$payload += $this->buildProspectPayload($activity);
$crmObjectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK
? Field::OBJECT_TASK
: Field::OBJECT_EVENT;
$this->logger->info('[Salesforce] Log activity. Payload Prepared', [
'activity_id' => $activity->getId(),
'object_type' => $playbook->getActivityType(),
'crm_object_type' => $crmObjectType,
'payload' => $payload,
]);
$this->saveCrmObject($crmObjectType, $activity, $payload);
return $activity;
}
private function buildSubjectPayload(Activity $activity, Playbook $playbook): array
{
if ($this->matchesCrmType($activity->getCategory())) {
/**
* If the activity playbook category name (i.e. activity type) matches a known activity type that exists
* in the CRM, then we set the activity type in the payload.
* Some activity types may be locally created. They shouldn't be sent to Salesforce.
*
* The resulting payload attribute will look like:
* `Type` => 'Cold Call'
*/
return [
/** ActivityFieldID => ActivityType */
$playbook->getActivityField()->getCrmProviderId() => $activity->getCategory()->getName(),
'Subject' => $this->getActivityDecorator()->generateTitle($activity),
];
}
/**
* Building of the title seems very inconsistent
* Subject here will be something like:
* "Local activity type: Activity Title"
*/
$subject = $activity->getCategory()->getName();
if ($activity->getTitle() !== null) {
$subject .= ': ' . $activity->getTitle();
}
return ['Subject' => $subject];
}
private function buildProspectPayload(Activity $activity): array
{
$payload = [];
if ($activity->getAccountId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'account_id' => $activity->getAccountId(),
'opportunity_id' => $activity->getOpportunityId(),
'contact_id' => $activity->getContactId(),
]);
$hasLinkFeature = $this->crmHelperRepository->hasFeature(
$activity,
FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS
);
if (($hasLinkFeature && $activity->getOpportunityId() && ! $activity->getContactId())
|| (! $hasLinkFeature && $activity->getOpportunityId())
) {
$payload['WhatId'] = $activity->getOpportunity()->getCrmProviderId();
} else {
$payload['WhatId'] = $activity->getAccount()->getCrmProviderId();
}
}
if ($activity->getContactId()) {
$this->logger->info('[Salesforce] Activity has account id', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$payload['WhoId'] = $activity->getContact()->getCrmProviderId();
} elseif ($activity->getLeadId()) {
$this->logger->info('[Salesforce] Activity has lead id', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->getLeadId(),
]);
/**
* An activity may have:
* a) a contact | account | opportunity (or combinations)
* b) only a lead
* c) none of the above
*
* An activity should not have a lead, and another prospect object at the same time.
*
* This logical flow should not happen naturally, but as a fallback: if an activity has both
* a Lead and an Account, the account's WhatId takes precedence in the CRM payload.
* The lead association will be detached locally, but the account WhatId is preserved in Salesforce.
*/
$payload += $this->handleActivityLead($activity);
}
return $payload;
}
private function handleActivityLead(Activity $activity): array
{
$this->logger->info('[Salesforce] Activity sync lead', [
'activity_id' => $activity->getId(),
]);
/**
* Sync lead to pull fresh data. Leads change often,
* they may get 'converted', and we must handle the new state accordingly
*/
try {
$activityLead = $this->syncLead(
$activity->getLead()->getCrmProviderId()
);
} catch (HttpNotFoundException) {
$this->logger->warning('[Salesforce] Activity lead is missing in Salesforce. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not found, maybe it's remotely deleted
*/
$this->resetLeadAssociation($activity);
return [];
}
if ($activityLead === null) {
$this->logger->warning('[Salesforce] Activity lead is missing in local DB. Resetting lead', [
'activity_id' => $activity->getId(),
]);
/**
* Lead is not loaded, for some reason. It may be remotely found, but locally deleted.
* Or an important Lead property may be missing, and it cannot be fully imported.
*/
$this->resetLeadAssociation($activity);
return [];
}
/**
* The lead is not converted, directly use it as WhoId and move on.
*/
if ($activityLead->getConvertedAt() === null) {
return [
'WhoId' => $activity->getLead()->getCrmProviderId(),
'WhatId' => null, // Clears WhatId on the remote record, unless an account WhatId was already set in the payload.
];
}
/**
* Task/Event payload properties for the CRM update
*/
$whoId = null;
$whatId = null;
/**
* Activity payload properties, that need to be updated.
*/
$convertedOpportunityId = null;
$convertedAccountId = null;
$convertedContactId = null;
$value = null;
/**
* If there is converted opportunity, it will update the activity stage.
* If there isn't an opportunity, a converted account/contact cannot have a stage id,
* so the activity shouldn't have a stage either.
*/
$stageId = null;
/**
* The lead is definitely converted.
* This means the lead may be associated with an opportunity, account, or contact.
* The converted lead itself must be detached from the activity,
* while the converted prospect objects must be associated instead,
* since they now represent the state of the deal.
*/
$activity->setLeadId(null);
/**
* CRM property "WhatId" can be either an Opportunity or an Account.
* For us a converted Opportunity has higher priority than an Account.
* An Opportunity carries more concrete data, including a reference to the Account record itself.
*/
if ($activityLead->hasOpportunity()) {
$convertedOpportunity = $activityLead->getOpportunity();
$convertedOpportunityId = $convertedOpportunity->getId();
$convertedAccountId = $convertedOpportunity->getAccountId();
$stageId = $convertedOpportunity->getStageId();
$value = $convertedOpportunity->getValue();
$whatId = $convertedOpportunity->getCrmProviderId();
}
if ($activityLead->hasAccount()) {
$convertedAccount = $activityLead->getAccount();
$convertedAccountId ??= $convertedAccount->getId();
$whatId ??= $convertedAccount->getCrmProviderId();
}
/**
* WhoId can only be a Contact or a Lead.
* Since we are detaching the Lead, it can only be a Contact for the current activity.
* It does not conflict with any other object.
*/
if ($activityLead->hasContact()) {
$convertedContact = $activityLead->getContact();
$convertedContactId = $convertedContact->getId();
$whoId = $convertedContact->getCrmProviderId();
}
$activity->setOpportunityId($convertedOpportunityId);
$activity->setAccountId($convertedAccountId);
$activity->setContactId($convertedContactId);
$activity->setStageId($stageId);
$activity->setValue($value);
$activity->save();
$this->eventDispatcher->dispatch(
new ActivityLeadConverted($activity, $activityLead)
);
$this->logger->info('[Salesforce] Lead is converted', [
'activity_id' => $activity->getId(),
'whoId' => $whoId,
'whatId' => $whatId,
]);
return [
'WhoId' => $whoId,
'WhatId' => $whatId,
];
}
private function saveCrmObject(string $objectType, Activity $activity, array $payload): void
{
/**
* Tasks and events have different payload structures,
* so we need to consider the type of the object we are sending to the CRM
*/
$specificPayload = $objectType === Field::OBJECT_TASK
? $this->payloadBuilder->buildTaskPayload($activity)
: $this->payloadBuilder->buildEventPayload($activity);
$payload = array_merge($payload, $specificPayload);
if ($activity->hasCrmProviderId()) {
$this->logger->info('[Salesforce] Updating crm object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
try {
$this->updateCrmActivity($objectType, $activity, $payload);
} catch (HttpNotFoundException) {
$this->logger->error('[Salesforce] Target crm object is missing in the crm. Resetting crm provider id', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$activity->setCrmProviderId(null);
$activity->save();
}
return;
}
$this->logger->info('[Salesforce] Creating CRM object', [
'activity_id' => $activity->getId(),
'crm_provider_id' => $activity->getCrmProviderId(),
'object_type' => $objectType,
]);
$crmProviderId = $this->createRecord($objectType, $payload);
$activity->setCrmProviderId($crmProviderId);
$activity->save();
}
/**
* @throws HttpNotFoundException
*/
private function updateCrmActivity(string $objectType, Activity $activity, array $payload): void
{
$sfActivity = $this->getRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
fields: ['Description', 'WhoId', 'WhatId']
);
if (! empty($payload['WhoId']) && $sfActivity['WhoId'] !== $payload['WhoId']) {
$this->logger->info('[Salesforce] Updating WhoId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
if (! empty($payload['WhatId']) && $sfActivity['WhatId'] !== $payload['WhatId']) {
$this->logger->info('[Salesforce] Updating WhatId', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
]);
} else {
$payload['WhatId'] = null;
}
}
$payload['Description'] = $this->getActivityDecorator()->mergeDescriptions(
$payload['Description'],
$sfActivity['Description'] ?? null
);
$this->logger->info('[Salesforce] Updating CRM activity data', [
'objectType' => $objectType,
'activityId' => $activity->getUuid(),
'crmActivity' => $sfActivity,
'payload' => $payload,
]);
$this->updateRecord(
objectType: $objectType,
objectId: $activity->getCrmProviderId(),
data: $payload
);
}
protected function getActivityDecorator(): DecorateActivity
{
if ($this->decorateActivity === null) {
$this->decorateActivity = app(DecorateActivity::class);
}
return $this->decorateActivity;
}
private function resetLeadAssociation(Activity $activity): void
{
$activity->setStageId(null);
$activity->setLeadId(null);
$activity->save();
}
}...
|
45449
|
NULL
|
NULL
|
NULL
|
|
45452
|
1627
|
4
|
2026-05-14T14:37:39.552341+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769459552_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <47FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:37:39* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:• JobsPlanhatService.php© AutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.phpProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php X© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php X© DataClient.php© Playbook.php© PipedrivService.php© DecorateActivity.php© PlainTextDecorateActivity.php~/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.phpmowoaniController.php=.env.staging18© LocalSearch.php• LocalSearchlnterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpC RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.php15.04.26 Vasilev24trait LogActivityTraitv C ListenersA6 X1л23.04.26 Vasilev85private function buildSubjectPayload(Activity $activity, Playbook $playbook): array© ConvertLeadActivities.php21.04.26 Vasilev90* in the CRM, then we set the activity type in the payload.© PurgeLookupCache.php21.04.26 Vasilev91* Some activity types may be locally created. They shouldn't be sent to Salesfol 216› D Metadata21.04.26 Vasilev92*> C Migration21.04.26 Vasilevv 0 Pipedrive93* The resulting payload attribute will look like:21.04.26Vasilev94Type=> 'Cold Call'> D OpportunitySyncStrategy21.04.26 Vasilev95*/> ProspectSearchStrategy23.04.26 Vasilev96return [© ApiFields.php23.04.26 Vasilev97/** ActivityFieldID => ActivityType */© Client.php15.04.26 Vasilev98$playbook->getActivityField()->getCrmProviderId() => $activity->getCategoryC.223© FieldDefinitions.php20.04.26 Vasilev99© PipedriveApiClient.php'Subject' => Sthis->getActivityDecorator()->generateTitle(Sactivity),15.04.26 Vasilev1001:• PipedriveApiException.php23.04.26 Vasilev101© Service.php15.04.26 Vasilev102© TokenStorage.php23.04.26 Vasilev103/**v D Salesforce23.04.26 Vasilev104* Building of the title seems very inconsistent• Fields23.04.26 Vasilev105* Subject here will be something like:• OpportunityMatcher23.04.26 Vasilev106*"Local activity type: Activity Title"• OpportunitySyncStrategy23.04.26 Vasilev107*/D ProspectSearchStrategy23.04.26 Vasilev108v • ServiceTraits$subject = Sactivity->getCategory(->getNameO:23.04.26 Vasilev109if (Sactivity->getTitle() |== null) {T BatchSyncTrait.php23.04.26 Vasilev110+ FollowupActivityTrait.php$subject .= ': ' . $activity->getTitle();15.04.26 Vasilev111T LogActivityTrait.php15.04.26 Vasilev112+ RecordManipulationsTrait./23.04.26 Vasilev113return ['Subject' => $subject];T SyncFieldsTrait.php23.04.26 Vasilev114@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)= custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v084143 ×4 л215•m_layout_id =2,1661,66799,66217218219id = 33;220221222224id THEN '(own225226227228229230hubspot':-231232233V= 11512582;234W Windsurf Teams98:92 (11 chars)UTF-8Ca 4 spaces...
|
NULL
|
-6973900021411041732
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <47FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:37:39* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:• JobsPlanhatService.php© AutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.phpProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php X© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php X© DataClient.php© Playbook.php© PipedrivService.php© DecorateActivity.php© PlainTextDecorateActivity.php~/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.phpmowoaniController.php=.env.staging18© LocalSearch.php• LocalSearchlnterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpC RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.php15.04.26 Vasilev24trait LogActivityTraitv C ListenersA6 X1л23.04.26 Vasilev85private function buildSubjectPayload(Activity $activity, Playbook $playbook): array© ConvertLeadActivities.php21.04.26 Vasilev90* in the CRM, then we set the activity type in the payload.© PurgeLookupCache.php21.04.26 Vasilev91* Some activity types may be locally created. They shouldn't be sent to Salesfol 216› D Metadata21.04.26 Vasilev92*> C Migration21.04.26 Vasilevv 0 Pipedrive93* The resulting payload attribute will look like:21.04.26Vasilev94Type=> 'Cold Call'> D OpportunitySyncStrategy21.04.26 Vasilev95*/> ProspectSearchStrategy23.04.26 Vasilev96return [© ApiFields.php23.04.26 Vasilev97/** ActivityFieldID => ActivityType */© Client.php15.04.26 Vasilev98$playbook->getActivityField()->getCrmProviderId() => $activity->getCategoryC.223© FieldDefinitions.php20.04.26 Vasilev99© PipedriveApiClient.php'Subject' => Sthis->getActivityDecorator()->generateTitle(Sactivity),15.04.26 Vasilev1001:• PipedriveApiException.php23.04.26 Vasilev101© Service.php15.04.26 Vasilev102© TokenStorage.php23.04.26 Vasilev103/**v D Salesforce23.04.26 Vasilev104* Building of the title seems very inconsistent• Fields23.04.26 Vasilev105* Subject here will be something like:• OpportunityMatcher23.04.26 Vasilev106*"Local activity type: Activity Title"• OpportunitySyncStrategy23.04.26 Vasilev107*/D ProspectSearchStrategy23.04.26 Vasilev108v • ServiceTraits$subject = Sactivity->getCategory(->getNameO:23.04.26 Vasilev109if (Sactivity->getTitle() |== null) {T BatchSyncTrait.php23.04.26 Vasilev110+ FollowupActivityTrait.php$subject .= ': ' . $activity->getTitle();15.04.26 Vasilev111T LogActivityTrait.php15.04.26 Vasilev112+ RecordManipulationsTrait./23.04.26 Vasilev113return ['Subject' => $subject];T SyncFieldsTrait.php23.04.26 Vasilev114@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)= custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v084143 ×4 л215•m_layout_id =2,1661,66799,66217218219id = 33;220221222224id THEN '(own225226227228229230hubspot':-231232233V= 11512582;234W Windsurf Teams98:92 (11 chars)UTF-8Ca 4 spaces...
|
45450
|
NULL
|
NULL
|
NULL
|
|
45453
|
1627
|
5
|
2026-05-14T14:37:41.144897+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769461144_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
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.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"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":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"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 Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\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 Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"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":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"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}]...
|
3382584868951134281
|
6551498163460912845
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45454
|
1628
|
4
|
2026-05-14T14:37:39.512113+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769459512_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.46 GB availabld• Inu 14 Mаy 1/•3/•34Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-4836291221105321116
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.46 GB availabld• Inu 14 Mаy 1/•3/•34Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45455
|
1628
|
5
|
2026-05-14T14:37:59.289683+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769479289_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
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.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"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.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"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":"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":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","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 Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"bounds":{"left":0.39261967,"top":0.17956904,"width":0.31615692,"height":0.820431},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"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":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.28224733,"top":1.0,"width":0.024268618,"height":-0.04788506},"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}]...
|
3382584868951134281
|
6551498163460912845
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
45454
|
NULL
|
NULL
|
NULL
|
|
45456
|
1627
|
6
|
2026-05-14T14:38:03.983210+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769483983_m1.jpg...
|
PhpStorm
|
faVsco.js – DecorateActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5840353787124240911
|
-7195274138713911831
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv100% C8• Thu 14 May 17:38:03* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:=custom.log+ SyncCrmEntitiesTrait.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.phpT SyncFieldsTrait.php+ WriteCrmTrait.php© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php x© Salesforce/Service.phpT LogActivityTrait.php> D Utils© Playbook.php© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.phpD WebhookT ActivityPlaybookTrait.php18.••© BatchSyncCollector.php© BatchSyncRedisService.php© DetachActivityObject.php© Client.php© ClosedDealStagesService.php© PlainTextDecorateActivity.php-env© HubspotPaginationService.phpHandleHubspotRateLimit.php5namespace Jiminny (Services\Crm Hubspot;© CrmHelperRepository.phpAccountController.php=.env.stagingRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.phpClient.php01 A2 A15 X12 ^G DealFieldsService.php© DecorateActivity.phpuse© FieldDefinitions.php© FieldTypeConverter.php© HubspotClientinterface.php121314classDecorateActivity extends BaseDecorateActivity© HubspotTokenManager.php5 usages© PayloadBuilder.php© RemoteCrmObjectManipulatoi© ResponseNormalize.php© Service.php© SyncFieldAction.php© SyncRelatedActivityManager.|© WebhookSyncBatchProcessolv D IntegrationApp• AccessorsD ApiD ConfigDDTOD Filters→ Jobs• ProspectSearchStrategy• ServiceTraits© DataClient.php15 C1617 ©181920|2122232425262728293045public const string JIMINNX_MARKER = 'Jiminny Summary:' ;public function generateTitle(Activity $activity): ?stringif ($activity->hasTitle()) {$title = $activity->getTitle();} elseif ($activity->hasActivityTypeO)) {$title = $activity->getActivityType() ->getName();} else {$title = 'Conference Call':return $title;public function convertMeetingOutcome(Activity Sactivity): ?string{...}© DecorateActivity.php2 usages© LocalSearch.php46public function convertCallStatus(Activity $activity): string{...}© LocalSearchlnterface.php55DamntaCaornhnhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]D68 jiminny084143 ×4 ^215•m_layout_id =2162,1661,66799,662172181 219id = 33;122281— 221|222=223— 224|id THEN •(own225226_227228229230hubspot':=231— 232-233V= 11512582;_234W Windsurf Teams17:21UTF-8Ca 4 spaces...
|
45453
|
NULL
|
NULL
|
NULL
|
|
45457
|
1627
|
7
|
2026-05-14T14:38:05.590243+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769485590_m1.jpg...
|
PhpStorm
|
faVsco.js – DecorateActivity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
15
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Illuminate\Support\Collection;
use Jiminny\Models\Activity;
use Jiminny\Repositories\ActivityMessageRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Crm\DecorateActivity\BaseDecorateActivity;
class DecorateActivity extends BaseDecorateActivity
{
public const string JIMINNY_MARKER = 'Jiminny Summary:';
public function generateTitle(Activity $activity): ?string
{
if ($activity->hasTitle()) {
$title = $activity->getTitle();
} elseif ($activity->hasActivityType()) {
$title = $activity->getActivityType()->getName();
} else {
$title = 'Conference Call';
}
return $title;
}
public function convertMeetingOutcome(Activity $activity): ?string
{
switch ($activity->getStatus()) {
case Activity::STATUS_SCHEDULED:
return 'SCHEDULED';
case Activity::STATUS_CANCELLED:
return 'CANCELED';
case Activity::STATUS_COMPLETED:
return 'COMPLETED';
//case Activity::STATUS_NO_SHOW:
// return 'NO_SHOW';
default:
return null;
}
}
public function convertCallStatus(Activity $activity): string
{
switch ($activity->getStatus()) {
case Activity::STATUS_NO_ANSWER:
return 'NO_ANSWER';
default:
return 'COMPLETED';
}
}
public function generateDescription(Activity $activity): ?string
{
if (in_array($activity->getType(), [Activity::TYPE_SMS_INBOUND, Activity::TYPE_SMS_OUTBOUND], true)) {
return $this->formatSmsDescription($activity);
}
$description = $this->baseDescription();
if ($activity->hasTitle() && $activity->hasActivityType()) {
$description .= $this->formatActivityType($activity->getActivityType()->getName());
}
if ($activity->getProvider() !== Activity::PROVIDER_UPLOADER) {
$description .= $this->formatSoftphoneDetails($activity);
}
$description .= $this->formatReasonOrPreview($activity);
if ($activity->isTypeConference()) {
$description .= $this->formatAttendeesList($activity);
}
return $description
. $this->formatNotes($activity)
. $this->formatMessages($activity)
. $this->formatSummary($activity);
}
public function mergeDescriptions(string $jiminnyDescription, ?string $existingCrmDescription): string
{
if (empty($existingCrmDescription)) {
return $jiminnyDescription;
}
if (! $this->isJiminnyDescription($existingCrmDescription)) {
return $existingCrmDescription . '<hr>' . $jiminnyDescription;
}
$existingSections = $this->parseHtmlSections($existingCrmDescription);
$newSections = $this->parseHtmlSections($jiminnyDescription);
return $this->mergeHtmlSections($newSections, $existingSections);
}
private function parseHtmlSections(string $html): array
{
$sections = [
'beforeJiminny' => '',
'header' => '',
];
foreach (self::SECTION_HEADERS as $name) {
$sections[$name] = '';
}
$headerStartPos = $this->findHeaderStartPosition($html);
$markerPos = stripos($html, self::JIMINNY_MARKER);
if ($headerStartPos !== false && $headerStartPos > 0) {
$sections['beforeJiminny'] = trim(substr($html, 0, $headerStartPos));
}
if ($headerStartPos !== false) {
$jiminnyStart = $headerStartPos;
} elseif ($markerPos !== false) {
$jiminnyStart = $markerPos;
} else {
$jiminnyStart = 0;
}
$firstSectionPos = $this->findFirstHtmlSectionPosition($html, $jiminnyStart);
if ($firstSectionPos === false) {
$sections['header'] = substr($html, $jiminnyStart);
} else {
$sections['header'] = substr($html, $jiminnyStart, $firstSectionPos - $jiminnyStart);
}
$sections['header'] = trim($sections['header']);
foreach (self::SECTION_HEADERS as $sectionName) {
$sections[$sectionName] = $this->extractHtmlSectionContent($html, $sectionName, $jiminnyStart);
}
return $sections;
}
private function findHeaderStartPosition(string $html): int|false
{
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*<span[^>]*>\s*' . preg_quote(self::JIMINNY_MARKER, '/') . '/i';
if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE)) {
return $matches[0][1];
}
$markerPos = stripos($html, self::JIMINNY_MARKER);
if ($markerPos === false) {
return false;
}
$beforeMarker = substr($html, 0, $markerPos);
$lastPTagPos = strrpos($beforeMarker, '<p');
return $lastPTagPos !== false ? $lastPTagPos : $markerPos;
}
private function findFirstHtmlSectionPosition(string $html, int $afterPosition = 0): int|false
{
$firstPos = false;
foreach (self::SECTION_HEADERS as $sectionName) {
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*' . preg_quote($sectionName, '/') . '\s*<\/strong>\s*<\/p>/i';
if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {
$pos = $matches[0][1];
if ($firstPos === false || $pos < $firstPos) {
$firstPos = $pos;
}
}
}
return $firstPos;
}
private function extractHtmlSectionContent(string $html, string $sectionName, int $afterPosition = 0): string
{
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*' . preg_quote($sectionName, '/') . '\s*<\/strong>\s*<\/p>/i';
if (! preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {
return '';
}
$headerMatch = $matches[0][0];
$headerPos = $matches[0][1];
$contentStart = $headerPos + strlen($headerMatch);
$nextSectionPos = $this->findNextHtmlSectionPosition($html, $contentStart);
if ($nextSectionPos === false) {
$content = substr($html, $contentStart);
} else {
$content = substr($html, $contentStart, $nextSectionPos - $contentStart);
}
return trim($content);
}
private function findNextHtmlSectionPosition(string $html, int $afterPosition): int|false
{
$nextPos = false;
foreach (self::SECTION_HEADERS as $sectionName) {
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*' . preg_quote($sectionName, '/') . '\s*<\/strong>\s*<\/p>/i';
if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {
$pos = $matches[0][1];
if ($nextPos === false || $pos < $nextPos) {
$nextPos = $pos;
}
}
}
return $nextPos;
}
private function mergeHtmlSections(array $newSections, array $existingSections): string
{
$result = '';
if (! empty($existingSections['beforeJiminny'])) {
$result .= $existingSections['beforeJiminny'];
}
$result .= $this->wrapHeader($newSections['header']);
foreach (self::SECTION_HEADERS as $sectionName) {
$existingContent = $existingSections[$sectionName] ?? '';
$newContent = $newSections[$sectionName] ?? '';
$content = ! empty($existingContent) ? $existingContent : $newContent;
if (! empty($content)) {
$result .= '<p><strong>' . $sectionName . '</strong></p>' . $content;
}
}
return $result;
}
private function wrapHeader(string $header): string
{
$header = trim($header);
if (str_starts_with($header, '<p>')) {
return $header;
}
if (str_starts_with($header, self::JIMINNY_MARKER)) {
return $this->baseDescription() . substr($header, strlen(self::JIMINNY_MARKER));
}
return $header;
}
private function baseDescription(): string
{
return sprintf(
'<p><strong><span style="font-size: 15px; line-height: 30px;">%s</span></strong></p>',
'Jiminny Summary:'
);
}
private function formatSoftphoneDetails(Activity $activity): string
{
$isOutbound = $activity->isTypeSoftPhone();
$isInbound = $activity->isTypeSoftphoneInbound();
if (! $isOutbound && ! $isInbound) {
return '';
}
$output = $this->formatTitle($activity->getTitle());
$output .= $this->formatCallDuration($activity->getAttribute('duration_for_humans'));
$phone = $isOutbound
? $activity->to?->phone_number
: $activity->from?->phone_number;
if ($phone) {
$output .= $isOutbound
? $this->formatDialedNumber($phone)
: $this->formatCallerNumber($phone);
}
return $output . '<p></p>';
}
private function formatReasonOrPreview(Activity $activity): string
{
if ($activity->hasReasonCodeBotKicked()) {
return '<p><span style="font-size: 13.3333px; line-height: 16px;">'
. 'Notetaker removed from this meeting'
. '</span></p>';
}
if ($activity->hasReasonCodeNotCompliant()) {
return '<p><span style="font-size: 13.3333px; line-height: 16px;">'
. 'Notetaker did not join due to recording consent not being provided by attendees'
. '</span></p>';
}
return $this->formatPreviewUrl($activity);
}
public function formatStartTime(Activity $activity): int
{
return $activity->getActualStartTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;
}
public function formatEndTime(Activity $activity): int
{
return $activity->getActualEndTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;
}
public function formatCreatedAt(Activity $activity): int
{
return $activity->getCreatedAt()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;
}
public function formatCallToParticipant(Activity $activity): ?string
{
$callToParticipant = $activity->getToParticipant()?->getPhoneNumber();
if ($callToParticipant && $activity->hasContact()) {
return $activity->getContact()->getName() . ' ' . $callToParticipant;
}
return null;
}
private function formatActivityType(string $activityType): string
{
return '<p><strong><span style="font-size: 18.6667px; line-height: 22.4px;">' . $activityType . '</span></strong></p>';
}
private function formatTitle(?string $title): string
{
return $title ? '<p><span style="font-size: 16px; line-height: 19.2px;">' . $title . '</span></p>' : '';
}
private function formatCallDuration(string $duration): string
{
return '<p><span style="font-size: 13.3333px; line-height: 16px;">Call Duration: ' . $duration . '</span></p>';
}
private function formatDialedNumber(string $phoneNumber): string
{
return '<p><span style="font-size: 13.3333px; line-height: 16px;">Dialed: ' . $phoneNumber . '</span></p>';
}
private function formatCallerNumber(string $phoneNumber): string
{
return '<p><span style="font-size: 13.3333px; line-height: 16px;">Caller: ' . $phoneNumber . '</span></p>';
}
private function formatAttendeesList(Activity $activity): string
{
$attendeesList = '<p><strong>Attendees</strong></p><ul>';
$participantsRepository = app(ParticipantRepository::class);
// Only include participants that joined.
$participants = $participantsRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
$attendee = '<li>';
if ($participant->hasName()) {
$attendee .= $participant->getName();
if ($participant->hasPhoneNumber()) {
$attendee .= ' (' . $participant->getPhoneNumber() . ')';
} elseif ($participant->hasEmailAddress()) {
$attendee .= ' (' . $participant->getEmailAddress() . ')';
}
} else {
$attendee .= $participant->getPhoneNumber();
}
if ($participant->isCoach()) {
$attendee .= ' <strong>coach</strong>';
}
if ($participant->getTitle()) {
$attendee .= ', ' . $participant->getTitle();
}
$attendeesList .= $attendee . '</li>';
}
return $attendeesList . '</ul>';
}
private function formatPreviewUrl(Activity $activity): string
{
$description = '';
$team = $activity->getUser()->getTeam();
if ($activity->canReviewActivity()) {
$playbackUrl = $team->getPartner()->getPlaybackUrl($activity);
$description .= '<p><span style="font-size: 13.3333px; line-height: 16px;"><a href="'
. $playbackUrl
. '" target="_blank">Review in Jiminny</a> ▶️</span></p>';
}
return $description;
}
private function formatNotes(Activity $activity): string
{
$description = '';
if ($activity->getNotes()->isEmpty()) {
return $description;
}
$description .= '<p><strong>Notes</strong></p>';
foreach ($activity->getNotes() as $note) {
$time = ($note->getTime() > 3600)
? gmdate('H:i:s', (int) $note->getTime())
: gmdate('i:s', (int) $note->getTime());
$description .= '<p>' . $time . ' ' . $note->getNote() . '</p>';
}
return $description . '</p>';
}
private function formatMessages(Activity $activity): string
{
$description = '';
$messageRepository = app(ActivityMessageRepository::class);
// Get all private messages.
$privateMessages = $messageRepository->getPrivateMessages($activity);
if ($privateMessages->isNotEmpty()) {
$description .= '<p><strong>Coaching Chat</strong></p>';
$description .= $this->getMessagesAsString($privateMessages);
$description .= '</p>';
}
// Get all public messages
$publicMessages = $messageRepository->getPublicMessages($activity);
if ($publicMessages->isNotEmpty()) {
$description .= '<p><strong>Customer Chat</strong></p>';
$description .= $this->getMessagesAsString($publicMessages);
$description .= '</p>';
}
return $description;
}
private function getMessagesAsString(Collection $messages): string
{
$description = '';
/** @var Activity\Message $message */
foreach ($messages as $message) {
$description .= '<p>' . $message->participant->getName()
. ': ' . $message->getAttribute('message') . '</p>';
}
return $description;
}
private function formatSummary(Activity $activity): string
{
$description = '';
if ($activity->getSummary() !== null) {
$description .= '<p><strong>Summary</strong></p><p>' . nl2br($activity->getSummary()) . '</p><p></p>';
}
return $description;
}
private function formatSmsDescription(Activity $activity): string
{
// Use Rich-text formatting.
$description = '<p><span style="font-size: 16px; line-height: 19.2px;"><strong>' . $activity->getTitle() . '</strong></span></p>';
$description .= '<p><span style="font-size: 16px; line-height: 19.2px;">' . $activity->getDescription() . '</span></p>';
return $description;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
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.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"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":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","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.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.73333335,"top":0.3122222,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.75277776,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.7736111,"top":0.3122222,"width":0.02013889,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.79791665,"top":0.3122222,"width":0.02013889,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Repositories\\ActivityMessageRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Crm\\DecorateActivity\\BaseDecorateActivity;\n\nclass DecorateActivity extends BaseDecorateActivity\n{\n public const string JIMINNY_MARKER = 'Jiminny Summary:';\n\n public function generateTitle(Activity $activity): ?string\n {\n if ($activity->hasTitle()) {\n $title = $activity->getTitle();\n } elseif ($activity->hasActivityType()) {\n $title = $activity->getActivityType()->getName();\n } else {\n $title = 'Conference Call';\n }\n\n return $title;\n }\n\n public function convertMeetingOutcome(Activity $activity): ?string\n {\n switch ($activity->getStatus()) {\n case Activity::STATUS_SCHEDULED:\n return 'SCHEDULED';\n case Activity::STATUS_CANCELLED:\n return 'CANCELED';\n case Activity::STATUS_COMPLETED:\n return 'COMPLETED';\n //case Activity::STATUS_NO_SHOW:\n // return 'NO_SHOW';\n default:\n return null;\n }\n }\n\n public function convertCallStatus(Activity $activity): string\n {\n switch ($activity->getStatus()) {\n case Activity::STATUS_NO_ANSWER:\n return 'NO_ANSWER';\n default:\n return 'COMPLETED';\n }\n }\n\n public function generateDescription(Activity $activity): ?string\n {\n if (in_array($activity->getType(), [Activity::TYPE_SMS_INBOUND, Activity::TYPE_SMS_OUTBOUND], true)) {\n return $this->formatSmsDescription($activity);\n }\n\n $description = $this->baseDescription();\n\n if ($activity->hasTitle() && $activity->hasActivityType()) {\n $description .= $this->formatActivityType($activity->getActivityType()->getName());\n }\n\n if ($activity->getProvider() !== Activity::PROVIDER_UPLOADER) {\n $description .= $this->formatSoftphoneDetails($activity);\n }\n\n $description .= $this->formatReasonOrPreview($activity);\n\n if ($activity->isTypeConference()) {\n $description .= $this->formatAttendeesList($activity);\n }\n\n return $description\n . $this->formatNotes($activity)\n . $this->formatMessages($activity)\n . $this->formatSummary($activity);\n }\n\n public function mergeDescriptions(string $jiminnyDescription, ?string $existingCrmDescription): string\n {\n if (empty($existingCrmDescription)) {\n return $jiminnyDescription;\n }\n\n if (! $this->isJiminnyDescription($existingCrmDescription)) {\n return $existingCrmDescription . '<hr>' . $jiminnyDescription;\n }\n\n $existingSections = $this->parseHtmlSections($existingCrmDescription);\n $newSections = $this->parseHtmlSections($jiminnyDescription);\n\n return $this->mergeHtmlSections($newSections, $existingSections);\n }\n\n private function parseHtmlSections(string $html): array\n {\n $sections = [\n 'beforeJiminny' => '',\n 'header' => '',\n ];\n\n foreach (self::SECTION_HEADERS as $name) {\n $sections[$name] = '';\n }\n\n $headerStartPos = $this->findHeaderStartPosition($html);\n $markerPos = stripos($html, self::JIMINNY_MARKER);\n\n if ($headerStartPos !== false && $headerStartPos > 0) {\n $sections['beforeJiminny'] = trim(substr($html, 0, $headerStartPos));\n }\n\n if ($headerStartPos !== false) {\n $jiminnyStart = $headerStartPos;\n } elseif ($markerPos !== false) {\n $jiminnyStart = $markerPos;\n } else {\n $jiminnyStart = 0;\n }\n\n $firstSectionPos = $this->findFirstHtmlSectionPosition($html, $jiminnyStart);\n\n if ($firstSectionPos === false) {\n $sections['header'] = substr($html, $jiminnyStart);\n } else {\n $sections['header'] = substr($html, $jiminnyStart, $firstSectionPos - $jiminnyStart);\n }\n\n $sections['header'] = trim($sections['header']);\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $sections[$sectionName] = $this->extractHtmlSectionContent($html, $sectionName, $jiminnyStart);\n }\n\n return $sections;\n }\n\n private function findHeaderStartPosition(string $html): int|false\n {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*<span[^>]*>\\s*' . preg_quote(self::JIMINNY_MARKER, '/') . '/i';\n\n if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE)) {\n return $matches[0][1];\n }\n\n $markerPos = stripos($html, self::JIMINNY_MARKER);\n if ($markerPos === false) {\n return false;\n }\n\n $beforeMarker = substr($html, 0, $markerPos);\n $lastPTagPos = strrpos($beforeMarker, '<p');\n\n return $lastPTagPos !== false ? $lastPTagPos : $markerPos;\n }\n\n private function findFirstHtmlSectionPosition(string $html, int $afterPosition = 0): int|false\n {\n $firstPos = false;\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*' . preg_quote($sectionName, '/') . '\\s*<\\/strong>\\s*<\\/p>/i';\n if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {\n $pos = $matches[0][1];\n if ($firstPos === false || $pos < $firstPos) {\n $firstPos = $pos;\n }\n }\n }\n\n return $firstPos;\n }\n\n private function extractHtmlSectionContent(string $html, string $sectionName, int $afterPosition = 0): string\n {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*' . preg_quote($sectionName, '/') . '\\s*<\\/strong>\\s*<\\/p>/i';\n\n if (! preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {\n return '';\n }\n\n $headerMatch = $matches[0][0];\n $headerPos = $matches[0][1];\n $contentStart = $headerPos + strlen($headerMatch);\n\n $nextSectionPos = $this->findNextHtmlSectionPosition($html, $contentStart);\n\n if ($nextSectionPos === false) {\n $content = substr($html, $contentStart);\n } else {\n $content = substr($html, $contentStart, $nextSectionPos - $contentStart);\n }\n\n return trim($content);\n }\n\n private function findNextHtmlSectionPosition(string $html, int $afterPosition): int|false\n {\n $nextPos = false;\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*' . preg_quote($sectionName, '/') . '\\s*<\\/strong>\\s*<\\/p>/i';\n if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {\n $pos = $matches[0][1];\n if ($nextPos === false || $pos < $nextPos) {\n $nextPos = $pos;\n }\n }\n }\n\n return $nextPos;\n }\n\n private function mergeHtmlSections(array $newSections, array $existingSections): string\n {\n $result = '';\n\n if (! empty($existingSections['beforeJiminny'])) {\n $result .= $existingSections['beforeJiminny'];\n }\n\n $result .= $this->wrapHeader($newSections['header']);\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $existingContent = $existingSections[$sectionName] ?? '';\n $newContent = $newSections[$sectionName] ?? '';\n\n $content = ! empty($existingContent) ? $existingContent : $newContent;\n\n if (! empty($content)) {\n $result .= '<p><strong>' . $sectionName . '</strong></p>' . $content;\n }\n }\n\n return $result;\n }\n\n private function wrapHeader(string $header): string\n {\n $header = trim($header);\n\n if (str_starts_with($header, '<p>')) {\n return $header;\n }\n\n if (str_starts_with($header, self::JIMINNY_MARKER)) {\n return $this->baseDescription() . substr($header, strlen(self::JIMINNY_MARKER));\n }\n\n return $header;\n }\n\n private function baseDescription(): string\n {\n return sprintf(\n '<p><strong><span style=\"font-size: 15px; line-height: 30px;\">%s</span></strong></p>',\n 'Jiminny Summary:'\n );\n }\n\n private function formatSoftphoneDetails(Activity $activity): string\n {\n $isOutbound = $activity->isTypeSoftPhone();\n $isInbound = $activity->isTypeSoftphoneInbound();\n\n if (! $isOutbound && ! $isInbound) {\n return '';\n }\n\n $output = $this->formatTitle($activity->getTitle());\n $output .= $this->formatCallDuration($activity->getAttribute('duration_for_humans'));\n\n $phone = $isOutbound\n ? $activity->to?->phone_number\n : $activity->from?->phone_number;\n\n if ($phone) {\n $output .= $isOutbound\n ? $this->formatDialedNumber($phone)\n : $this->formatCallerNumber($phone);\n }\n\n return $output . '<p></p>';\n }\n\n private function formatReasonOrPreview(Activity $activity): string\n {\n if ($activity->hasReasonCodeBotKicked()) {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">'\n . 'Notetaker removed from this meeting'\n . '</span></p>';\n }\n\n if ($activity->hasReasonCodeNotCompliant()) {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">'\n . 'Notetaker did not join due to recording consent not being provided by attendees'\n . '</span></p>';\n }\n\n return $this->formatPreviewUrl($activity);\n }\n\n\n public function formatStartTime(Activity $activity): int\n {\n return $activity->getActualStartTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;\n }\n\n public function formatEndTime(Activity $activity): int\n {\n return $activity->getActualEndTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;\n }\n\n public function formatCreatedAt(Activity $activity): int\n {\n return $activity->getCreatedAt()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;\n }\n\n public function formatCallToParticipant(Activity $activity): ?string\n {\n $callToParticipant = $activity->getToParticipant()?->getPhoneNumber();\n if ($callToParticipant && $activity->hasContact()) {\n return $activity->getContact()->getName() . ' ' . $callToParticipant;\n }\n\n return null;\n }\n\n private function formatActivityType(string $activityType): string\n {\n return '<p><strong><span style=\"font-size: 18.6667px; line-height: 22.4px;\">' . $activityType . '</span></strong></p>';\n }\n\n private function formatTitle(?string $title): string\n {\n return $title ? '<p><span style=\"font-size: 16px; line-height: 19.2px;\">' . $title . '</span></p>' : '';\n }\n\n private function formatCallDuration(string $duration): string\n {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">Call Duration: ' . $duration . '</span></p>';\n }\n\n private function formatDialedNumber(string $phoneNumber): string\n {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">Dialed: ' . $phoneNumber . '</span></p>';\n }\n\n private function formatCallerNumber(string $phoneNumber): string\n {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">Caller: ' . $phoneNumber . '</span></p>';\n }\n\n private function formatAttendeesList(Activity $activity): string\n {\n $attendeesList = '<p><strong>Attendees</strong></p><ul>';\n\n $participantsRepository = app(ParticipantRepository::class);\n // Only include participants that joined.\n $participants = $participantsRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n $attendee = '<li>';\n if ($participant->hasName()) {\n $attendee .= $participant->getName();\n if ($participant->hasPhoneNumber()) {\n $attendee .= ' (' . $participant->getPhoneNumber() . ')';\n } elseif ($participant->hasEmailAddress()) {\n $attendee .= ' (' . $participant->getEmailAddress() . ')';\n }\n } else {\n $attendee .= $participant->getPhoneNumber();\n }\n\n if ($participant->isCoach()) {\n $attendee .= ' <strong>coach</strong>';\n }\n\n if ($participant->getTitle()) {\n $attendee .= ', ' . $participant->getTitle();\n }\n\n $attendeesList .= $attendee . '</li>';\n }\n\n return $attendeesList . '</ul>';\n }\n\n private function formatPreviewUrl(Activity $activity): string\n {\n $description = '';\n $team = $activity->getUser()->getTeam();\n\n if ($activity->canReviewActivity()) {\n $playbackUrl = $team->getPartner()->getPlaybackUrl($activity);\n $description .= '<p><span style=\"font-size: 13.3333px; line-height: 16px;\"><a href=\"'\n . $playbackUrl\n . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>';\n }\n\n return $description;\n }\n\n private function formatNotes(Activity $activity): string\n {\n $description = '';\n if ($activity->getNotes()->isEmpty()) {\n return $description;\n }\n\n $description .= '<p><strong>Notes</strong></p>';\n foreach ($activity->getNotes() as $note) {\n $time = ($note->getTime() > 3600)\n ? gmdate('H:i:s', (int) $note->getTime())\n : gmdate('i:s', (int) $note->getTime());\n\n $description .= '<p>' . $time . ' ' . $note->getNote() . '</p>';\n }\n\n return $description . '</p>';\n }\n\n private function formatMessages(Activity $activity): string\n {\n $description = '';\n $messageRepository = app(ActivityMessageRepository::class);\n\n // Get all private messages.\n $privateMessages = $messageRepository->getPrivateMessages($activity);\n if ($privateMessages->isNotEmpty()) {\n $description .= '<p><strong>Coaching Chat</strong></p>';\n $description .= $this->getMessagesAsString($privateMessages);\n $description .= '</p>';\n }\n\n // Get all public messages\n $publicMessages = $messageRepository->getPublicMessages($activity);\n if ($publicMessages->isNotEmpty()) {\n $description .= '<p><strong>Customer Chat</strong></p>';\n $description .= $this->getMessagesAsString($publicMessages);\n $description .= '</p>';\n }\n\n return $description;\n }\n\n private function getMessagesAsString(Collection $messages): string\n {\n $description = '';\n\n /** @var Activity\\Message $message */\n foreach ($messages as $message) {\n $description .= '<p>' . $message->participant->getName()\n . ': ' . $message->getAttribute('message') . '</p>';\n }\n\n return $description;\n }\n\n private function formatSummary(Activity $activity): string\n {\n $description = '';\n if ($activity->getSummary() !== null) {\n $description .= '<p><strong>Summary</strong></p><p>' . nl2br($activity->getSummary()) . '</p><p></p>';\n }\n\n return $description;\n }\n\n private function formatSmsDescription(Activity $activity): string\n {\n // Use Rich-text formatting.\n $description = '<p><span style=\"font-size: 16px; line-height: 19.2px;\"><strong>' . $activity->getTitle() . '</strong></span></p>';\n $description .= '<p><span style=\"font-size: 16px; line-height: 19.2px;\">' . $activity->getDescription() . '</span></p>';\n\n return $description;\n }\n}","depth":4,"bounds":{"left":0.26666668,"top":0.21111111,"width":0.6548611,"height":0.7888889},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Repositories\\ActivityMessageRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Crm\\DecorateActivity\\BaseDecorateActivity;\n\nclass DecorateActivity extends BaseDecorateActivity\n{\n public const string JIMINNY_MARKER = 'Jiminny Summary:';\n\n public function generateTitle(Activity $activity): ?string\n {\n if ($activity->hasTitle()) {\n $title = $activity->getTitle();\n } elseif ($activity->hasActivityType()) {\n $title = $activity->getActivityType()->getName();\n } else {\n $title = 'Conference Call';\n }\n\n return $title;\n }\n\n public function convertMeetingOutcome(Activity $activity): ?string\n {\n switch ($activity->getStatus()) {\n case Activity::STATUS_SCHEDULED:\n return 'SCHEDULED';\n case Activity::STATUS_CANCELLED:\n return 'CANCELED';\n case Activity::STATUS_COMPLETED:\n return 'COMPLETED';\n //case Activity::STATUS_NO_SHOW:\n // return 'NO_SHOW';\n default:\n return null;\n }\n }\n\n public function convertCallStatus(Activity $activity): string\n {\n switch ($activity->getStatus()) {\n case Activity::STATUS_NO_ANSWER:\n return 'NO_ANSWER';\n default:\n return 'COMPLETED';\n }\n }\n\n public function generateDescription(Activity $activity): ?string\n {\n if (in_array($activity->getType(), [Activity::TYPE_SMS_INBOUND, Activity::TYPE_SMS_OUTBOUND], true)) {\n return $this->formatSmsDescription($activity);\n }\n\n $description = $this->baseDescription();\n\n if ($activity->hasTitle() && $activity->hasActivityType()) {\n $description .= $this->formatActivityType($activity->getActivityType()->getName());\n }\n\n if ($activity->getProvider() !== Activity::PROVIDER_UPLOADER) {\n $description .= $this->formatSoftphoneDetails($activity);\n }\n\n $description .= $this->formatReasonOrPreview($activity);\n\n if ($activity->isTypeConference()) {\n $description .= $this->formatAttendeesList($activity);\n }\n\n return $description\n . $this->formatNotes($activity)\n . $this->formatMessages($activity)\n . $this->formatSummary($activity);\n }\n\n public function mergeDescriptions(string $jiminnyDescription, ?string $existingCrmDescription): string\n {\n if (empty($existingCrmDescription)) {\n return $jiminnyDescription;\n }\n\n if (! $this->isJiminnyDescription($existingCrmDescription)) {\n return $existingCrmDescription . '<hr>' . $jiminnyDescription;\n }\n\n $existingSections = $this->parseHtmlSections($existingCrmDescription);\n $newSections = $this->parseHtmlSections($jiminnyDescription);\n\n return $this->mergeHtmlSections($newSections, $existingSections);\n }\n\n private function parseHtmlSections(string $html): array\n {\n $sections = [\n 'beforeJiminny' => '',\n 'header' => '',\n ];\n\n foreach (self::SECTION_HEADERS as $name) {\n $sections[$name] = '';\n }\n\n $headerStartPos = $this->findHeaderStartPosition($html);\n $markerPos = stripos($html, self::JIMINNY_MARKER);\n\n if ($headerStartPos !== false && $headerStartPos > 0) {\n $sections['beforeJiminny'] = trim(substr($html, 0, $headerStartPos));\n }\n\n if ($headerStartPos !== false) {\n $jiminnyStart = $headerStartPos;\n } elseif ($markerPos !== false) {\n $jiminnyStart = $markerPos;\n } else {\n $jiminnyStart = 0;\n }\n\n $firstSectionPos = $this->findFirstHtmlSectionPosition($html, $jiminnyStart);\n\n if ($firstSectionPos === false) {\n $sections['header'] = substr($html, $jiminnyStart);\n } else {\n $sections['header'] = substr($html, $jiminnyStart, $firstSectionPos - $jiminnyStart);\n }\n\n $sections['header'] = trim($sections['header']);\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $sections[$sectionName] = $this->extractHtmlSectionContent($html, $sectionName, $jiminnyStart);\n }\n\n return $sections;\n }\n\n private function findHeaderStartPosition(string $html): int|false\n {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*<span[^>]*>\\s*' . preg_quote(self::JIMINNY_MARKER, '/') . '/i';\n\n if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE)) {\n return $matches[0][1];\n }\n\n $markerPos = stripos($html, self::JIMINNY_MARKER);\n if ($markerPos === false) {\n return false;\n }\n\n $beforeMarker = substr($html, 0, $markerPos);\n $lastPTagPos = strrpos($beforeMarker, '<p');\n\n return $lastPTagPos !== false ? $lastPTagPos : $markerPos;\n }\n\n private function findFirstHtmlSectionPosition(string $html, int $afterPosition = 0): int|false\n {\n $firstPos = false;\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*' . preg_quote($sectionName, '/') . '\\s*<\\/strong>\\s*<\\/p>/i';\n if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {\n $pos = $matches[0][1];\n if ($firstPos === false || $pos < $firstPos) {\n $firstPos = $pos;\n }\n }\n }\n\n return $firstPos;\n }\n\n private function extractHtmlSectionContent(string $html, string $sectionName, int $afterPosition = 0): string\n {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*' . preg_quote($sectionName, '/') . '\\s*<\\/strong>\\s*<\\/p>/i';\n\n if (! preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {\n return '';\n }\n\n $headerMatch = $matches[0][0];\n $headerPos = $matches[0][1];\n $contentStart = $headerPos + strlen($headerMatch);\n\n $nextSectionPos = $this->findNextHtmlSectionPosition($html, $contentStart);\n\n if ($nextSectionPos === false) {\n $content = substr($html, $contentStart);\n } else {\n $content = substr($html, $contentStart, $nextSectionPos - $contentStart);\n }\n\n return trim($content);\n }\n\n private function findNextHtmlSectionPosition(string $html, int $afterPosition): int|false\n {\n $nextPos = false;\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $pattern = '/<p[^>]*>\\s*<strong[^>]*>\\s*' . preg_quote($sectionName, '/') . '\\s*<\\/strong>\\s*<\\/p>/i';\n if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {\n $pos = $matches[0][1];\n if ($nextPos === false || $pos < $nextPos) {\n $nextPos = $pos;\n }\n }\n }\n\n return $nextPos;\n }\n\n private function mergeHtmlSections(array $newSections, array $existingSections): string\n {\n $result = '';\n\n if (! empty($existingSections['beforeJiminny'])) {\n $result .= $existingSections['beforeJiminny'];\n }\n\n $result .= $this->wrapHeader($newSections['header']);\n\n foreach (self::SECTION_HEADERS as $sectionName) {\n $existingContent = $existingSections[$sectionName] ?? '';\n $newContent = $newSections[$sectionName] ?? '';\n\n $content = ! empty($existingContent) ? $existingContent : $newContent;\n\n if (! empty($content)) {\n $result .= '<p><strong>' . $sectionName . '</strong></p>' . $content;\n }\n }\n\n return $result;\n }\n\n private function wrapHeader(string $header): string\n {\n $header = trim($header);\n\n if (str_starts_with($header, '<p>')) {\n return $header;\n }\n\n if (str_starts_with($header, self::JIMINNY_MARKER)) {\n return $this->baseDescription() . substr($header, strlen(self::JIMINNY_MARKER));\n }\n\n return $header;\n }\n\n private function baseDescription(): string\n {\n return sprintf(\n '<p><strong><span style=\"font-size: 15px; line-height: 30px;\">%s</span></strong></p>',\n 'Jiminny Summary:'\n );\n }\n\n private function formatSoftphoneDetails(Activity $activity): string\n {\n $isOutbound = $activity->isTypeSoftPhone();\n $isInbound = $activity->isTypeSoftphoneInbound();\n\n if (! $isOutbound && ! $isInbound) {\n return '';\n }\n\n $output = $this->formatTitle($activity->getTitle());\n $output .= $this->formatCallDuration($activity->getAttribute('duration_for_humans'));\n\n $phone = $isOutbound\n ? $activity->to?->phone_number\n : $activity->from?->phone_number;\n\n if ($phone) {\n $output .= $isOutbound\n ? $this->formatDialedNumber($phone)\n : $this->formatCallerNumber($phone);\n }\n\n return $output . '<p></p>';\n }\n\n private function formatReasonOrPreview(Activity $activity): string\n {\n if ($activity->hasReasonCodeBotKicked()) {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">'\n . 'Notetaker removed from this meeting'\n . '</span></p>';\n }\n\n if ($activity->hasReasonCodeNotCompliant()) {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">'\n . 'Notetaker did not join due to recording consent not being provided by attendees'\n . '</span></p>';\n }\n\n return $this->formatPreviewUrl($activity);\n }\n\n\n public function formatStartTime(Activity $activity): int\n {\n return $activity->getActualStartTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;\n }\n\n public function formatEndTime(Activity $activity): int\n {\n return $activity->getActualEndTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;\n }\n\n public function formatCreatedAt(Activity $activity): int\n {\n return $activity->getCreatedAt()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;\n }\n\n public function formatCallToParticipant(Activity $activity): ?string\n {\n $callToParticipant = $activity->getToParticipant()?->getPhoneNumber();\n if ($callToParticipant && $activity->hasContact()) {\n return $activity->getContact()->getName() . ' ' . $callToParticipant;\n }\n\n return null;\n }\n\n private function formatActivityType(string $activityType): string\n {\n return '<p><strong><span style=\"font-size: 18.6667px; line-height: 22.4px;\">' . $activityType . '</span></strong></p>';\n }\n\n private function formatTitle(?string $title): string\n {\n return $title ? '<p><span style=\"font-size: 16px; line-height: 19.2px;\">' . $title . '</span></p>' : '';\n }\n\n private function formatCallDuration(string $duration): string\n {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">Call Duration: ' . $duration . '</span></p>';\n }\n\n private function formatDialedNumber(string $phoneNumber): string\n {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">Dialed: ' . $phoneNumber . '</span></p>';\n }\n\n private function formatCallerNumber(string $phoneNumber): string\n {\n return '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">Caller: ' . $phoneNumber . '</span></p>';\n }\n\n private function formatAttendeesList(Activity $activity): string\n {\n $attendeesList = '<p><strong>Attendees</strong></p><ul>';\n\n $participantsRepository = app(ParticipantRepository::class);\n // Only include participants that joined.\n $participants = $participantsRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n $attendee = '<li>';\n if ($participant->hasName()) {\n $attendee .= $participant->getName();\n if ($participant->hasPhoneNumber()) {\n $attendee .= ' (' . $participant->getPhoneNumber() . ')';\n } elseif ($participant->hasEmailAddress()) {\n $attendee .= ' (' . $participant->getEmailAddress() . ')';\n }\n } else {\n $attendee .= $participant->getPhoneNumber();\n }\n\n if ($participant->isCoach()) {\n $attendee .= ' <strong>coach</strong>';\n }\n\n if ($participant->getTitle()) {\n $attendee .= ', ' . $participant->getTitle();\n }\n\n $attendeesList .= $attendee . '</li>';\n }\n\n return $attendeesList . '</ul>';\n }\n\n private function formatPreviewUrl(Activity $activity): string\n {\n $description = '';\n $team = $activity->getUser()->getTeam();\n\n if ($activity->canReviewActivity()) {\n $playbackUrl = $team->getPartner()->getPlaybackUrl($activity);\n $description .= '<p><span style=\"font-size: 13.3333px; line-height: 16px;\"><a href=\"'\n . $playbackUrl\n . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>';\n }\n\n return $description;\n }\n\n private function formatNotes(Activity $activity): string\n {\n $description = '';\n if ($activity->getNotes()->isEmpty()) {\n return $description;\n }\n\n $description .= '<p><strong>Notes</strong></p>';\n foreach ($activity->getNotes() as $note) {\n $time = ($note->getTime() > 3600)\n ? gmdate('H:i:s', (int) $note->getTime())\n : gmdate('i:s', (int) $note->getTime());\n\n $description .= '<p>' . $time . ' ' . $note->getNote() . '</p>';\n }\n\n return $description . '</p>';\n }\n\n private function formatMessages(Activity $activity): string\n {\n $description = '';\n $messageRepository = app(ActivityMessageRepository::class);\n\n // Get all private messages.\n $privateMessages = $messageRepository->getPrivateMessages($activity);\n if ($privateMessages->isNotEmpty()) {\n $description .= '<p><strong>Coaching Chat</strong></p>';\n $description .= $this->getMessagesAsString($privateMessages);\n $description .= '</p>';\n }\n\n // Get all public messages\n $publicMessages = $messageRepository->getPublicMessages($activity);\n if ($publicMessages->isNotEmpty()) {\n $description .= '<p><strong>Customer Chat</strong></p>';\n $description .= $this->getMessagesAsString($publicMessages);\n $description .= '</p>';\n }\n\n return $description;\n }\n\n private function getMessagesAsString(Collection $messages): string\n {\n $description = '';\n\n /** @var Activity\\Message $message */\n foreach ($messages as $message) {\n $description .= '<p>' . $message->participant->getName()\n . ': ' . $message->getAttribute('message') . '</p>';\n }\n\n return $description;\n }\n\n private function formatSummary(Activity $activity): string\n {\n $description = '';\n if ($activity->getSummary() !== null) {\n $description .= '<p><strong>Summary</strong></p><p>' . nl2br($activity->getSummary()) . '</p><p></p>';\n }\n\n return $description;\n }\n\n private function formatSmsDescription(Activity $activity): string\n {\n // Use Rich-text formatting.\n $description = '<p><span style=\"font-size: 16px; line-height: 19.2px;\"><strong>' . $activity->getTitle() . '</strong></span></p>';\n $description .= '<p><span style=\"font-size: 16px; line-height: 19.2px;\">' . $activity->getDescription() . '</span></p>';\n\n return $description;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"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":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","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.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"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}]...
|
6902378850207333842
|
9001456016336028185
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
15
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Illuminate\Support\Collection;
use Jiminny\Models\Activity;
use Jiminny\Repositories\ActivityMessageRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Crm\DecorateActivity\BaseDecorateActivity;
class DecorateActivity extends BaseDecorateActivity
{
public const string JIMINNY_MARKER = 'Jiminny Summary:';
public function generateTitle(Activity $activity): ?string
{
if ($activity->hasTitle()) {
$title = $activity->getTitle();
} elseif ($activity->hasActivityType()) {
$title = $activity->getActivityType()->getName();
} else {
$title = 'Conference Call';
}
return $title;
}
public function convertMeetingOutcome(Activity $activity): ?string
{
switch ($activity->getStatus()) {
case Activity::STATUS_SCHEDULED:
return 'SCHEDULED';
case Activity::STATUS_CANCELLED:
return 'CANCELED';
case Activity::STATUS_COMPLETED:
return 'COMPLETED';
//case Activity::STATUS_NO_SHOW:
// return 'NO_SHOW';
default:
return null;
}
}
public function convertCallStatus(Activity $activity): string
{
switch ($activity->getStatus()) {
case Activity::STATUS_NO_ANSWER:
return 'NO_ANSWER';
default:
return 'COMPLETED';
}
}
public function generateDescription(Activity $activity): ?string
{
if (in_array($activity->getType(), [Activity::TYPE_SMS_INBOUND, Activity::TYPE_SMS_OUTBOUND], true)) {
return $this->formatSmsDescription($activity);
}
$description = $this->baseDescription();
if ($activity->hasTitle() && $activity->hasActivityType()) {
$description .= $this->formatActivityType($activity->getActivityType()->getName());
}
if ($activity->getProvider() !== Activity::PROVIDER_UPLOADER) {
$description .= $this->formatSoftphoneDetails($activity);
}
$description .= $this->formatReasonOrPreview($activity);
if ($activity->isTypeConference()) {
$description .= $this->formatAttendeesList($activity);
}
return $description
. $this->formatNotes($activity)
. $this->formatMessages($activity)
. $this->formatSummary($activity);
}
public function mergeDescriptions(string $jiminnyDescription, ?string $existingCrmDescription): string
{
if (empty($existingCrmDescription)) {
return $jiminnyDescription;
}
if (! $this->isJiminnyDescription($existingCrmDescription)) {
return $existingCrmDescription . '<hr>' . $jiminnyDescription;
}
$existingSections = $this->parseHtmlSections($existingCrmDescription);
$newSections = $this->parseHtmlSections($jiminnyDescription);
return $this->mergeHtmlSections($newSections, $existingSections);
}
private function parseHtmlSections(string $html): array
{
$sections = [
'beforeJiminny' => '',
'header' => '',
];
foreach (self::SECTION_HEADERS as $name) {
$sections[$name] = '';
}
$headerStartPos = $this->findHeaderStartPosition($html);
$markerPos = stripos($html, self::JIMINNY_MARKER);
if ($headerStartPos !== false && $headerStartPos > 0) {
$sections['beforeJiminny'] = trim(substr($html, 0, $headerStartPos));
}
if ($headerStartPos !== false) {
$jiminnyStart = $headerStartPos;
} elseif ($markerPos !== false) {
$jiminnyStart = $markerPos;
} else {
$jiminnyStart = 0;
}
$firstSectionPos = $this->findFirstHtmlSectionPosition($html, $jiminnyStart);
if ($firstSectionPos === false) {
$sections['header'] = substr($html, $jiminnyStart);
} else {
$sections['header'] = substr($html, $jiminnyStart, $firstSectionPos - $jiminnyStart);
}
$sections['header'] = trim($sections['header']);
foreach (self::SECTION_HEADERS as $sectionName) {
$sections[$sectionName] = $this->extractHtmlSectionContent($html, $sectionName, $jiminnyStart);
}
return $sections;
}
private function findHeaderStartPosition(string $html): int|false
{
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*<span[^>]*>\s*' . preg_quote(self::JIMINNY_MARKER, '/') . '/i';
if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE)) {
return $matches[0][1];
}
$markerPos = stripos($html, self::JIMINNY_MARKER);
if ($markerPos === false) {
return false;
}
$beforeMarker = substr($html, 0, $markerPos);
$lastPTagPos = strrpos($beforeMarker, '<p');
return $lastPTagPos !== false ? $lastPTagPos : $markerPos;
}
private function findFirstHtmlSectionPosition(string $html, int $afterPosition = 0): int|false
{
$firstPos = false;
foreach (self::SECTION_HEADERS as $sectionName) {
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*' . preg_quote($sectionName, '/') . '\s*<\/strong>\s*<\/p>/i';
if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {
$pos = $matches[0][1];
if ($firstPos === false || $pos < $firstPos) {
$firstPos = $pos;
}
}
}
return $firstPos;
}
private function extractHtmlSectionContent(string $html, string $sectionName, int $afterPosition = 0): string
{
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*' . preg_quote($sectionName, '/') . '\s*<\/strong>\s*<\/p>/i';
if (! preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {
return '';
}
$headerMatch = $matches[0][0];
$headerPos = $matches[0][1];
$contentStart = $headerPos + strlen($headerMatch);
$nextSectionPos = $this->findNextHtmlSectionPosition($html, $contentStart);
if ($nextSectionPos === false) {
$content = substr($html, $contentStart);
} else {
$content = substr($html, $contentStart, $nextSectionPos - $contentStart);
}
return trim($content);
}
private function findNextHtmlSectionPosition(string $html, int $afterPosition): int|false
{
$nextPos = false;
foreach (self::SECTION_HEADERS as $sectionName) {
$pattern = '/<p[^>]*>\s*<strong[^>]*>\s*' . preg_quote($sectionName, '/') . '\s*<\/strong>\s*<\/p>/i';
if (preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE, $afterPosition)) {
$pos = $matches[0][1];
if ($nextPos === false || $pos < $nextPos) {
$nextPos = $pos;
}
}
}
return $nextPos;
}
private function mergeHtmlSections(array $newSections, array $existingSections): string
{
$result = '';
if (! empty($existingSections['beforeJiminny'])) {
$result .= $existingSections['beforeJiminny'];
}
$result .= $this->wrapHeader($newSections['header']);
foreach (self::SECTION_HEADERS as $sectionName) {
$existingContent = $existingSections[$sectionName] ?? '';
$newContent = $newSections[$sectionName] ?? '';
$content = ! empty($existingContent) ? $existingContent : $newContent;
if (! empty($content)) {
$result .= '<p><strong>' . $sectionName . '</strong></p>' . $content;
}
}
return $result;
}
private function wrapHeader(string $header): string
{
$header = trim($header);
if (str_starts_with($header, '<p>')) {
return $header;
}
if (str_starts_with($header, self::JIMINNY_MARKER)) {
return $this->baseDescription() . substr($header, strlen(self::JIMINNY_MARKER));
}
return $header;
}
private function baseDescription(): string
{
return sprintf(
'<p><strong><span style="font-size: 15px; line-height: 30px;">%s</span></strong></p>',
'Jiminny Summary:'
);
}
private function formatSoftphoneDetails(Activity $activity): string
{
$isOutbound = $activity->isTypeSoftPhone();
$isInbound = $activity->isTypeSoftphoneInbound();
if (! $isOutbound && ! $isInbound) {
return '';
}
$output = $this->formatTitle($activity->getTitle());
$output .= $this->formatCallDuration($activity->getAttribute('duration_for_humans'));
$phone = $isOutbound
? $activity->to?->phone_number
: $activity->from?->phone_number;
if ($phone) {
$output .= $isOutbound
? $this->formatDialedNumber($phone)
: $this->formatCallerNumber($phone);
}
return $output . '<p></p>';
}
private function formatReasonOrPreview(Activity $activity): string
{
if ($activity->hasReasonCodeBotKicked()) {
return '<p><span style="font-size: 13.3333px; line-height: 16px;">'
. 'Notetaker removed from this meeting'
. '</span></p>';
}
if ($activity->hasReasonCodeNotCompliant()) {
return '<p><span style="font-size: 13.3333px; line-height: 16px;">'
. 'Notetaker did not join due to recording consent not being provided by attendees'
. '</span></p>';
}
return $this->formatPreviewUrl($activity);
}
public function formatStartTime(Activity $activity): int
{
return $activity->getActualStartTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;
}
public function formatEndTime(Activity $activity): int
{
return $activity->getActualEndTime()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;
}
public function formatCreatedAt(Activity $activity): int
{
return $activity->getCreatedAt()->tz($activity->getUser()->timezone)->getTimestamp() * 1000;
}
public function formatCallToParticipant(Activity $activity): ?string
{
$callToParticipant = $activity->getToParticipant()?->getPhoneNumber();
if ($callToParticipant && $activity->hasContact()) {
return $activity->getContact()->getName() . ' ' . $callToParticipant;
}
return null;
}
private function formatActivityType(string $activityType): string
{
return '<p><strong><span style="font-size: 18.6667px; line-height: 22.4px;">' . $activityType . '</span></strong></p>';
}
private function formatTitle(?string $title): string
{
return $title ? '<p><span style="font-size: 16px; line-height: 19.2px;">' . $title . '</span></p>' : '';
}
private function formatCallDuration(string $duration): string
{
return '<p><span style="font-size: 13.3333px; line-height: 16px;">Call Duration: ' . $duration . '</span></p>';
}
private function formatDialedNumber(string $phoneNumber): string
{
return '<p><span style="font-size: 13.3333px; line-height: 16px;">Dialed: ' . $phoneNumber . '</span></p>';
}
private function formatCallerNumber(string $phoneNumber): string
{
return '<p><span style="font-size: 13.3333px; line-height: 16px;">Caller: ' . $phoneNumber . '</span></p>';
}
private function formatAttendeesList(Activity $activity): string
{
$attendeesList = '<p><strong>Attendees</strong></p><ul>';
$participantsRepository = app(ParticipantRepository::class);
// Only include participants that joined.
$participants = $participantsRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
$attendee = '<li>';
if ($participant->hasName()) {
$attendee .= $participant->getName();
if ($participant->hasPhoneNumber()) {
$attendee .= ' (' . $participant->getPhoneNumber() . ')';
} elseif ($participant->hasEmailAddress()) {
$attendee .= ' (' . $participant->getEmailAddress() . ')';
}
} else {
$attendee .= $participant->getPhoneNumber();
}
if ($participant->isCoach()) {
$attendee .= ' <strong>coach</strong>';
}
if ($participant->getTitle()) {
$attendee .= ', ' . $participant->getTitle();
}
$attendeesList .= $attendee . '</li>';
}
return $attendeesList . '</ul>';
}
private function formatPreviewUrl(Activity $activity): string
{
$description = '';
$team = $activity->getUser()->getTeam();
if ($activity->canReviewActivity()) {
$playbackUrl = $team->getPartner()->getPlaybackUrl($activity);
$description .= '<p><span style="font-size: 13.3333px; line-height: 16px;"><a href="'
. $playbackUrl
. '" target="_blank">Review in Jiminny</a> ▶️</span></p>';
}
return $description;
}
private function formatNotes(Activity $activity): string
{
$description = '';
if ($activity->getNotes()->isEmpty()) {
return $description;
}
$description .= '<p><strong>Notes</strong></p>';
foreach ($activity->getNotes() as $note) {
$time = ($note->getTime() > 3600)
? gmdate('H:i:s', (int) $note->getTime())
: gmdate('i:s', (int) $note->getTime());
$description .= '<p>' . $time . ' ' . $note->getNote() . '</p>';
}
return $description . '</p>';
}
private function formatMessages(Activity $activity): string
{
$description = '';
$messageRepository = app(ActivityMessageRepository::class);
// Get all private messages.
$privateMessages = $messageRepository->getPrivateMessages($activity);
if ($privateMessages->isNotEmpty()) {
$description .= '<p><strong>Coaching Chat</strong></p>';
$description .= $this->getMessagesAsString($privateMessages);
$description .= '</p>';
}
// Get all public messages
$publicMessages = $messageRepository->getPublicMessages($activity);
if ($publicMessages->isNotEmpty()) {
$description .= '<p><strong>Customer Chat</strong></p>';
$description .= $this->getMessagesAsString($publicMessages);
$description .= '</p>';
}
return $description;
}
private function getMessagesAsString(Collection $messages): string
{
$description = '';
/** @var Activity\Message $message */
foreach ($messages as $message) {
$description .= '<p>' . $message->participant->getName()
. ': ' . $message->getAttribute('message') . '</p>';
}
return $description;
}
private function formatSummary(Activity $activity): string
{
$description = '';
if ($activity->getSummary() !== null) {
$description .= '<p><strong>Summary</strong></p><p>' . nl2br($activity->getSummary()) . '</p><p></p>';
}
return $description;
}
private function formatSmsDescription(Activity $activity): string
{
// Use Rich-text formatting.
$description = '<p><span style="font-size: 16px; line-height: 19.2px;"><strong>' . $activity->getTitle() . '</strong></span></p>';
$description .= '<p><span style="font-size: 16px; line-height: 19.2px;">' . $activity->getDescription() . '</span></p>';
return $description;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|