|
87267
|
rapstomCoocWindowFV faVsco.|s ~#12121 on JY-20963- rapstomCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lproideto kemotcennos coowanoeResponseNormalize.phoMmacuryochyict.ono service oneesunchicracoon.onGSUNCKOlOhCWWWKeweonooinlneochhoe>D IntearationAod>O Listeners>& Metadata>Da Pipedrivev SalestorceaFeldsa opoortunitvVatchenOpportunitySyncStrategyaProsoectSearchStrateo• SericettraitsC) e entond© DecorateActivity.cholnaldtsor weetihooooead natioitionenno© PayloadBuilder.phpc) Profile.php© QueryBuilder.phpceuanthodidenon© Queryiterator.pho© QueryResults.phpo Service.ond© SyncBatchRedisService.ptTraitelRasedlientond© BaseService.php© CachedCrmServiceDecoratoconiooekesowod) CrmActivityProviderinteorateCCINTCMINMOronoweauiwostenee3Findsproscectinterace.on(C). LavoutManscer.oho3MatchDomsinsvamsinteraeC Ooportur vActvtwwatche3oportun tySyncStratsovintc @nportun tySyncStratcoyeec Prosoectenche.ond@ ProsoectSearchScooc.nhocOrnenontCostchCtttammDrnenontCostrhCtratomcntoei orawdoroo etr nhnTO0У L7Thu 28 May 18:30:50ServiceTest+0.OpportunitySyncTrait.php© CrmEntityRepository.phgclass servace excenos baseservace aolesencspublic function inportStages(?array Stypes = null, ?string SmissingStageNane = null): ?Stage?string SmissingStageNane = null): ?StageD):// Stages - fetch all existing stages upfront to avoid N+1 queriesSexistnastaces = sthisescontgoastagesi.thiirashecioheretynp'Srace.owee ipetetlltesaetOt-›keуву( crn_provider_10):foreach (Spl'stages") as Sdealstage) 1$s = ResponseNormalize::normalizeDealStage($dealStage):/** Qvac ?Stage SexistingStage */SexistingStage = SexistingStages->get($s["id")):// Restore soft-deleted stages that are now active in HubSpot1f (SexistingStage?->trashed() && Ss['active']) {sexsensraceorestorer1 Upsert stage (updates soft-deleted records without restoring them.Sstage = Sthis->config->stages@->withTrashed@->update0rCreateCochaorovcoro ooh"tean_id""Labela=Th1s-cam->o.=> nb-strinwidth(Ssi'label") 8. S0).=> nb_strinwidth(Ssf'label"] 8, 191).= Stage::TYPE_OPPORTUNTTYasosolavirder'Sis selectable' a>› Ssf'active'1a>sorobabtuty *188.DA3€ (SnissinoStacelane eue Saleal)dSnissingStage = Sstage;Sstages[] = Sstage->id;SbusinessProcess-›stages()->sync(Sstages):pinolinaeSadnofnt?Extract Surround 1 E = Lent->getInstance()-getCLient()-grequest( mathod: iGST, Sentin:SF jiminny@localhostA console (PROD]A console (EU) X uin users (EUA console [STAGING)Oojiminny031 49 A29 У 3 У 109 A 1SELECT DISTINCT u.íd, u.enail, u.name, u.tcan_id, t.nane as team_namet.twilio sns sid.FROM users ut.twilio_nessaging_sidINNER JOIN Teans T 1..n<->1. ON U.rean 10 = 1.70HERE ETWIUO SES SIdS NOT NULL OR TITWROTOReSSaGInO.SIdS NOT NUEIROER SYTnare Mhenatiee Ran tonD NERE No LnE SReale) 257, 257, 530, лtertere оeane*ownen sd Fonl sockal accounts sInTM eene non muad= sarsochable ouncosteon tade mteanHERF Iteam 3d = 187 and sa-nrovsden = Isalacfonca*sereet * from contate n hehe to = 5 31256367;"d = 1SAATD"select * from opportunitieswhere id = 4843618"AAADAAL &A'= 1771/76# 'stage_id' = 13273,,"updated at" = 2826-85-22 87:16:17 where "id' = 312.select * from text_relays wherecreated_at > '2826-85-01';select * from activities order by id desc;select * from users wherenane Like 'KSubrak».SELECT * FROM opportunities WHERE uvid_to_bin('84a9cfad-2087-4453-9e72-28aeb78ccf8d'2select * from teans where d = 555%selec * from stages where tean 1d = 555CONCAT(u.id, CASE WHEN u.1d = t.owner_id THEN • (onner)' ELSE •* END) AS user_id=173e174sa.*,t.ouner_id FROM social_accounts saTantм nsene mon 1.4d = sa-socsable 3.1..n<->1: on t.id = u.tean_icWHERE v.team_id = SSS and sa.provider = 'hubspot":Cascadeles Orcnnworeebeeionhnves ohtine oDonRitoootaNorenhooer.orobabuty" s Tloat (icatalcetadata"orobabto tym)hThis ks the root causdHere's the full explanation:Root eause: moorStaces overwntes DB orobabilty with Huospot's valueThe flow is1. Someone manually changed stage 20616's probability in the Jiminny DB to 100.00 on 2826-45-44 . The stage crm provider 1d: "closedtost" isHubSpot's standard "Closed Lost" stage which natively has probability ® in HubSpot.2. ImportStages() runs (during any pipeline/stage sync), It callsService.php:445-454Sstage = Sthis->config-ostages()-withTrashed()->update0rCreate(lerorowideressiPbbttedcetorosbliew'113. sattorobabsTtu"1 comas from RestonscNorenKaer.roms WrebenlStaoe"orobability" a (4loat) (Saataf"retadata"|("orobabfWity'122 1.HubSpot returns netadata.probability = e for closedlost. So $s['probability") * 100 • 0. This resets stage 20616's probability back to 0 in tho4. Now getClosedDealStaces() outs "closedlost" into lostll (orobability 0), not woni5. The deal syncs → is_von = false, is_closed = truetha connonss of muonte.May 4After May 4Stace 20616 probability manually changed to 100 in Jiminny DRmortStanes () runr → resetr staae 20616 probability back to 0 (frem HubSpot)May 20Dral synos → *closedlost» is in lostll (orob=0) → ts won = falarThe opportunitye atane ta in etill 20616 (the stage record ID never changed), but by the time it suncad on May 20, iroortStages had already reset theprobability of that stage back to 0. So when bui ldOpportunityData runs, "closedlost" is in lost(I, not von!!The core issue: inportStages always overwrites the DB probability with whatever HubSpot says. Any manual probability customisation in Jiminny getswiped out the next time stages are synoed from HubSpot. Stage 20616 was manually set to 100 but the label says "7. Won (Contract Signed)" while theHibSoot em omuider fa le cloesdilost = HibSootie nativa orobability for that is 0uso it cate racarGuntaAt anthientua$ AdhotsM Wiedsud Teams 307-20 (4147 chars. 85 Fon braskel UTE-R Di/A enod...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
87267
|
|
87266
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// 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
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// 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();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDeals/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}PipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_p...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
87266
|
|
87265
|
PhpStormproidetViewNeweNNCCoocWindowFV faVsco.s#12 PhpStormproidetViewNeweNNCCoocWindowFV faVsco.s#12121 on JY-20963-fx-lo kemotcennos coowanoeResponseNormalize.phoosevice.onesunchiorcdon.onGSUNCKOlOhCWWWKeweonooinlneochhoe>D IntearationAod>O Listeners>& Metadata> D Migration>Da Pipedrivev Salesforce•DFeldsa opoortunitvVatchenOpportunity SyncStrategyaProsoectSearchStrateoeSemicetiraitsC) e entond© DecorateActivity.cholnaldtsor weetihoooê Giadnafinitione nho© PayloadBuilder.phpc) Profile.php© QueryBuilder.php© QueryHandler.php© Queryiterator.pho© QueryResults.phpe Sarnce oho© SyncBatchRedisService.ptTraitel© BaseClient.php© BaseService.php© CachedCrmServiceDecoratoconiooekesowod€ CrmActivitvProviderinteorateCCINTCMINMOron© CrmObjectsResolver.phpoweauiwosten© EmailHeloer.che3Findsproscectinterace.onC.LavoutManacer.oho3 MatchDomsinsvams nterseC Opportun tvActvityMatchereoportun tySyncStratcowntc @nportun tySyncStratcoyeec Prosoectenche.ond© ProspectSearchScope.phpci DrnenontCostchCtttamA PrnenontSonrch Gtetondoto(© ProviderRegistry.phpDorarriColontor nhoMmacuryochyict.onA console (EU) X uin users (EUA console [STAGING)336383TOA389392s 6.SSGA© RecordSelector.phgActivity.phoA console (PROD]OpportunitySyncTrait.php© CrmEntityRepository.phgA7 A149 V1/3311 A2Oojiminny031 49 A29 У 3 У109 A V167%public function inportPicklistValues(Field Sfieldarray Soptions = [['id' => **, 'label' => **."value' => "']]): array f...17821732SELECT DISTINCT u.íd, u.enail, u.name, u.tcan_id, t.nane as team_namet.twilio_sns_sid. t.twilfo.messaging.sidFROM users uINNER JOTN toams t 1..n<->1: ON u.tean id = t.idWHERE (t.twiliosns sid IS NOT NULL OR t.twilio_nessaging_sid TS NOT NULLD111RoEk dynare Mhenasi* RinheritdocSELECT * FROM teams WHERE nane LIKE '%Tounlane*': # 187, 209, 8150, salesforce-adningt.SELECTpublic function inportStages(Parray Stypes = null. ?string SmissingStageNane = null)): ?StagelCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN • (ouner)' ELSE •* END) AS user_id,SmissingStage = null:sa.*t.owner_id FROM social_accounts saInTM eene non muad= sarsochable o# Use the HubSpot APT client instead of the SDK cr-Pinelines() nethodSendooint = self::qetdecisPinelinesEndooint0:SpipelinesResponse = Sthis->client-›getInstance()-›getClient()->request ( methodSpinelines = SpipelinesResponse-sdata-sresults:1714-1710doointr112171717)InTM teasettuncoste on tfd e moteanWHERE u.team_id = 187 and sa.provider = 'salesfonce':calact * Erom actávstfas nbere 1d = 7124/347%catch (Request Exceotionl BadRequest sexceoton)uthrow Sexceot.ion.-110|Foreach (Soioelaines as sospebne 3uSstages = 1hors 1d = 145AA72.soreet * from oesouturthes vhe - 46 + 4243610;accountid' = 4156632."contactid' = 6331639opportunity# 'stage-id" = 13273actsvitios'. "updated.at* = 2926-85-22 87:16:17 where "1d* = 3120select * from text relays where created at > *2826-05-01*;// We create a business process to contain the pipeline, and store all stages against it.Sp = ResponseNornalize::nonmalszePipeline(SpipeLine):Tioanta nants huesinsee noocnee ton thhe ahnblbneSbusinessProcess = Sthis->config->businessProcesses()->update0rCreateC['crm_provider_id' => $p["id'],MIO T1726select * from activities order by id desciselect * from usens where nane Like "XSubrak':Sthfeostoseosidl=> mb_ strimwidth(Sp[ "label']start: e=> BusinessProcess::TYPE_OPPORTUNITY,'is selectable'=> Sp['active'1.D):17K6// Create/uodate record type clondSthis->config->recordtvoes@-suodate0rCreate(fteansio= Sthis-stean-sid.lSis selectable"e> olactave"Antity Il Mew oul reouest trodas 18:10)TO0У L7Thu 28 May 18:30:45ServiceTest+0.Cascadeles Orcnnworeeoeionhnves ohtine oDonRitoootaNorenhooer.orobabuty" s Tloat (Coatal"cetadata""nrobabto tyteohThis s the root causeHere's the full explanation:Root eause: moorStages overwmes D3 probability with tuoSpots valueThe flow is1. Someone manually changed stage 20616's probability in the Jiminny DB to 100.00 on 2826-45-44 . The stage crm provider 1d: "closedtost" isHubSpot's standard "Closed Lost" stage which natively has probability ® in HubSpot.2. ImportStages() runs (during any pipeline/stage sync), It callsService.php:445-454Sstage = Sthis->config-ostages()-withTrashed()->update0rCreate(lerorowideressitorobabsl4oucetorosbliew'113. sattorobabsTtu"1 comas from RestonscNorenKaer.roms WrebenlStaoe"orobability" a (4loat) (Saataf"retadata"|("orobabfWity'122 1.HubSpot returns netadata.probability = e for closedlost. So $s['probability") * 100 • 0. This resets stage 20616's probability back to 0 in tho4. Now getClosedDealStaces() outs "closedlost" into lostll (orobability 0), not woni5. The deal syncs → is_von = false, is_closed = truetha connonss of muonte.May 4After May 4Stace 20616 probability manually changed to 100 in Jiminny DAmortStanes () runr → resetr staae 20616 probability back to 0 (frem HubSpot)May 20Dral synos → *closedlost» is in lostll (orob=0) = ts won = falarThe opportunitye atane ta in etill 20616 (the stage reeord ID never changed), but by the time it suncad on May 20, iroortStages had already resct theprobability of that stage back to 0. So when bui ldOpportunityData runs, "closedlost" is in lost(I, not won!!The core issue: inportStages always overwrites the DB probability with whatever HubSpot says. Any manual probability customisation in Jiminny getswiped out the next time stages are synoed from HubSpot. Stage 20616 was manually set to 100 but the label says *7. Won (Contract Signed)" while thiHibSoot em omuider da le cloadilost = HibSootte nativa orobability for that is Ouso ft cate racatGuntaAt anthientua$ AdhotsMw Windsud Teams 393-5/2597 chars. 85 Fon braskel UTE-R Ai/A enod...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
87265
|
|
87264
|
IAlSlackFileEditViewGoHistoryWindowActivity Monito IAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUkernel_taskPhpStormWindowServerreplaydPostman Helper (Renderer)screenpipemds_storesFirefoxCP Isolated Web Contentcoreaudiodlanguage_server_macos_armcef_server Helper (Renderer)ClaudebluetoothdCursorUlViewService (Not Responding)Activity MonitorlaunchservicesdFirefoxCP Isolated Web ContentPostmanPostman Helper (GPU)FirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentFirefoxWispr FlowBitwardenFirefoxCP Isolated Web ContentiTerm2FirefoxCP Isolated Web Contentlaunchd150,4124,156,552,825,224,814,610,26,05,34,34,34,03,73,63,32,92,82,82,52,42,42,22,12,01,81,81,7HelpCPU Time24:06:25,834:21:06,768:07:07,286:40:44,887:00,043:50:26,761:10:33,5541:07,941:01:53,0512:01,299:39,4329:41,4821:06,036:03,589:32,511:04:25,3413:38,533:49,0737,9417:00,1558,371:49:59,234:24,642:32,2220:03,031:05:01,4611:52,2119:36,41System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp ,61%51,69%0,70%HomeDMsActivityFilesLater..•More+ED→Jiminny...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana NetsevaEo Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor Stamatov. Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:45Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилоример)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
NULL
|
87264
|
|
87263
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lCSamviceTesconr=custom.loglaravei.io"A SF jiminny@localhostHSJocal [EMAIL]© RecordSelector.phgO ActMily prA console (PRODasaasforce/Service.oneA console (EU) X uin users (EU# console [STAGINGM.INTERNALWEBHOOK SETU? maliminny storaceOpportunitySyncTrait.php© CrmEntityRepository.phgclass service excenos baseservace olesents87 8149 Y1Y33 214Oojiminny031 A9 A29 Y 3 109 A VMalicenses.maMakere0 package-lock.jsonphpstan.neon.distF phpstan-baseline.neonphpunit.xmlraw.sol query.saMAesNeiMs mapubuzc tunccion anooristades tarray suypes & nult, tsurane sssangscagenane & nulle escadeselect * from text relays where created at > 2826-85-01*÷4484494504S4"tean_1d=> Sthis->tean->1d,nane=> ab_strinuidth(Ssl"label"),Siorto,width: 50)'Label"=> ab_strinuidth(Ss['label')width: 191)'type=> Stage::TYPE_OPPORTUNITY,seauence"=> $s["display0rder'),'is_selectable' => $s['active')proodozersy=> $s['probability") * 100Seloe* o acewioes oroer oyooeseSELE * FROMopportunItles WHERE UUTCTO_OIN 84a9c.ad-2087-4453-9072-28aeb/8ccf80i sonar-project propertiesEtest.py<> Untitied Diagram.xmlus vetur.config.isMAWISAHOeK SinEoiNGMOSUND:SELEMIif (SnissingStageNane === $s["id')) 4CONCAT(u.1d, CASE WHEN U.1d = t.owner_1d THEN' (ouner)" ELSE"" END) AS USer_1d,Duanostooetodehnt. Sytemallloranesv =0 Cemtches and Consolosv @ Database ConsolesustacesudostacrosvAtU# console cUA DEAL RISKS (EUSousiness?rocess-sstaces@-svnclsstages.r31 ВО0 00 01000100000 11sa.*townen o sror social accounts seJOIN users u on u.id = sa.sociable.1ohiTihtassetAHERE U.team_id = 555 and sa.provider = 'hubspot":meutsun111 В 1Е 1146НSarvicaei+oc|ex7 Outpurjiminny.opportunitiesIID fiminny stages(D Result 12vontameAraww400E6A console 1 $ 650 mgui users 1 $ 548 msy2liminny@localhostA HSJJocalA SFV A PROD# ConcalaV A STAGINGH Concal"noakoruser idMlenailDidsociable_1c18826 (oaner)alex-nogunhtcoontewecroont.con3859118924provider user id47827227i provider user token1CPnogfionNx11201NOM8KOF.nAC.ACAKUAh7JBBZRA0F0Bxi7sO0M/WuothYo/KwCMhSF070VpK2Cn07061Z012u6097AToy01 NOM8K0FunAFUACBk6awEFTthwBARYBAOFFATEEA0FBAOFBAOFBAOUBFGOBA0EBAY1.CFEpRV10P2.0#RhN0cP4DQtvZYBUISeMR-IIESAFOAYABom65/2FnAAnAFprovider refresh-tokeneu1-8633-482c-4614-aae8-S88da6aß92ehEeYos nes.innatnach token ayasaneB provideri stateauthscopeMretry aftenconnectedcnulta<nullsM ccested.at2825-4-18 14:884113B updated.at292k09Cr284:2.2provider user token encryptedIeyJpd¡[6T?NXeFR"bT2RbG1yZUE2cVpQQnE4aẞc9PSIsInZhbHVLẞoÎdktIMnZ2UGY3L8t423E1TnVJNkxD0U9XWTRTSnVDCmhßKz©2Uk5HThmYU9гbkM22HFvSИRNUL?MRG1·M2ttRU43SHRqRHJRVStvYßdKK3FnUFFWFhpV21GWnIvVHRVZz840kxXMG1YcBRSaHZnY304U*L2VVBDSTDprovider refresh token encryptedleyJod$t6InMcUHh6Z2F. 24BxUF2TOHN3ZEtEWWC9PSIsInZhbHVLTo1aUs5L2BXWW9GT2dcZBdhd102a105SMocN1B1eHFCaTK5TENnVnNPV2h0YnF0b1U3eWV=YmscU0hnV2VKCEp2dCTST@1hYy26ImU3NTNSN5gcNnTxNWE1M2NkND112WNmMGUSY2B&YnYSOTBÄYTZmZGFmMmYy2TY321Mlencnyattion key (Hex)LBx SA75290E40RERE0O2REEGE:CBAERZ0SQ.ARZPATR.7/.601283254521R40597BAB1232AA0295315E / ServiceTestTO0У L7• Thu 28 May 18:30:42+0.meesdales Orcnnworeebeeionhnves ohtine oDonPitocottoe nhcoo..orobabuty" s Tloat (icatalcetadata"orobabto tym)hThis s the root causeHere's the full explanationRoot eause:moorStages overwmes D8 probabllty with Huosoots valueThe flow is1. Someone manually changed stage 20616's probability in the Jiminny DB to 100.00 on 2826-45-44. The stage crn provider 1d: "elosedtost" k2. ImportStages() runs (during any pipeline/stage sync), It callsService.php:445-454Sstage = Sthis->config-ostages()-withTrashed()->update0rCreate(ferprowderesto cetorosbliewr1+ <> CodeS? AdaotivCSV.TTTO0GIUOSMMald Tosma124009UTG.8Aenod...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87263
|
|
87262
|
SlackFileEditViewGoHistoryWindowActivity MonitorAl SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskscreenpipereplaydWindowServerPostman Helper (Renderer)PostmanFirefoxCP Isolated Web ContentClaudecoreaudiodPostman Helper (GPU)bluetoothdlanguage_server_macos_armFirefoxActivity MonitorlaunchservicesdFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr FlowSlack Helper (Renderer)FirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentSlackFirefoxCP Isolated Web ContentiTerm2Karabiner-Core-Service141,1110,455,751,441,934,715,89,05,75,63,93,73,23,23,23,02,92,62,42,02,02,01,91,91,81,81,71,6HelpCPU Time4:21:00,2124:06:17,893:50:25,456:40:42,108:07:04,316:58,713:48,9241:07,4029:41,251:01:52,7337.7921:05,8212:01,011:49:59,109:32,321:04:25,1743:54,8924:11,2317:00,014:24,521:00:44,4358,2520:02,9211:52,1114:05,274:23,191:05:01,3620:52,46System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+47,98%38,44%13,59%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...#: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:43Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилоример)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87262
|
|
87261
|
SlackFileEditViewGoHistoryWindowActivity MonitorAl SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskscreenpipereplaydWindowServerPostman Helper (Renderer)PostmanFirefoxCP Isolated Web ContentClaudecoreaudiodPostman Helper (GPU)PSPhpStormFiretoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr FlowSlack Helper (Renderer)FirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentSlackFirefoxCP Isolated Web ContentiTerm2Karabiner-Core-Service141,1110,455,751,441,934,715,89,05,75,63,92,62,42,02,02,01,91,91,81,81,71,6HelpCPU Time4:21:00,2124:06:17,893:50:25,456:40:42,108:07:04,316:58,713:48,9241:07,4029:41,251:01:52,7337,7924:11,2317:00,014:24,521:00:44,4358,2520:02,9211:52,1114:05,274:23,191:05:01,3620:52,46System:User:Idle:Threads CPUMemoryIdle Wake-Ups2478597563304121261865797$I16132411647EnelKpHomeDMsActivity100% <78• Thu 28 May 18:30:41ED→QDescribe what you are looking forJiminny ...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support®• lliyana Netseva• MessagesAdd canvas+една заявка (Asked hиилоример)Todayтука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsI.leae Vavalile c.ea neeN+24,15%35,58%40,27%CPUF8. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovR. Steliyan Georgievto VesG. MiraP.. Nikolay YankovdoJames GrahamE Lukas Kovalik y...::: AppsJira CloudLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva+...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87261
|
|
87260
|
IAlSlackFileEditViewGoHistoryWindowHelpActivity Mo IAlSlackFileEditViewGoHistoryWindowHelpActivity MonitorAll ProcessesProcess Name% CPUCPU TimePhpStormkernel_taskWindowServerPostman Helper (Renderer)screenpipereplaydFirefoxCP Isolated Web ContentPostmancoreaudiodClaudeFirefoxPostman Helper (GPU)bluetoothdActivity MonitorFirefoxCP Isolated Web Contentlanguage_server_macos_armFirefoxCP Isolated Web ContentlaunchservicesdFirefoxCP Isolated Web ContentiTerm2Wispr FlowFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentKarabiner-Core-ServiceWispr Flow Helper (Renderer)FirefoxPostman HelperControl Centre128,44:20:52,76125,454,142,724:06:12,078:07:02,096:56,8841,635,73:50:22,516:40:39,3810,541:06,933:48,091:01:52,4329:40,951:49:58,9337,5921:05,629:32,1513:38,2912:00,8458,141:04:25,0116:59,891:05:01,274:24,4120:02,8232:28,3920:52,385:43,3114:27,5955,2020:47,43System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+36,76%50,24%13,00%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana NetsevaEo Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko Kashinski. Galya Dimitrova: Todor Stamatov. Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...#: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:37Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилоример)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87260
|
|
87259
|
PostmanVieWwurcowX HubspotYour team is now on the PostmanVieWwurcowX HubspotYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationProperties > crm/yklorcosrties criact voe> Resd all oropertiesV COLLECYIONSEngsgements› HubspotKera don runttaration nin Sastch HeOaRWeonocoksVeOAuthcrm/v3/properties/(object Type>a groups?-olOoery Namev ut Read all oropartiesee successtul operationenAn error occurred.Rotcteae8 orooey• ЗLАKCHTickarsv Uselupest lilter por comanny /onty ocen dent stnoeywnendademens oo associa.tovoCii anasdsminte did actocitio ou comode.> ut get history of property - deal stageut detusersCET SF osuth> eaT Moatina cutcomes der mectind> cEt Read all properties newCin Rand alll oronartiaeoelct old call dispositionseflist with associationsсET list engagements okert recent enaaaement.cet get dealMiGatemadsmentmiE Docs Params • Authorization Headers 8 Body ScriptsSettinasQuery ParamsKeynapixeyorchiveePakh VarsolesobjectTypoRescanse" Hнom yPATCH httos://api.hubapi.com/engagements/v1/engagements/24927395231patckhttocdllaolhubant.com/crm/v3lob.ecte/meet0os/24927/3952311PATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231ert Uprste desiciandCET OLD doal by idATOUD contaetCMMIDANIENTCSPECS> FLOWSOConndettCoasde em anioo7o0sLXThu 28 May 18:30:36@ Searcherpendonea menim m sordCoe hitor nonteOET ReadCedet meetino# SaveSondCookiesBulk Ed?ValutfaiseValueWhether to retum only results that have boen archivedDescriptionRoottodRutke Shoet Sand e Get a upcessful resoons2) Send + Visualize respons)% Send + Write test.Giobnts Montt Tookemem...
|
iTerm2
|
NULL
|
NULL
|
87259
|
|
87258
|
PostmanVieWwurcowX HubspotwYour team is now on the PostmanVieWwurcowX HubspotwYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationProperties > crm/yklorcosrties crict voe> Resd all oropertiesV COLLECYIONSEnasgements› HubspotKera don runttaration nin Sastch HeOaR WeonocokseOAuthcrm/v3/properties/(object Type>a groups?-olOoy Namev ut Read all oropartiesee successtul operationenAn error occurred.Rotcteae8 orooey• ЗLАKCHTickarsv Uselu> pest llter por comaany / oniy ocen dent athoeawnendademens do associa.tovoCii anasdsminte did actochtid tu comode.> ct get history of property - deal stageut detusersCET SF osuth> est Mostina cutcomes per mectind> cEt Read all properties newCin Randlalll orooartine oct old call dispositionseflist with associationsсET list engagements olert recent enaaaement.cet get dealMiGatemadsmentmiE Docs Params • Authorization Headers 8 Body ScriptsSettinasQuery ParamsKeynapixeyorchiveePakh VarsolesobjectTypoRescanse" Hнom yPATCH httos://api.hubapi.com/engagements/v1/engagements/24927395231patckhttocdllaolhubant.com/crm/v3/ob/ecte/meet0os/24927/3952311PATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231ert Uprste desiciandCET OLD doal by idCTOUD contaetCMMIDANNENTeSPECS> FLOWSOConndettCoasde em anioo@ Searchcrpendeoea menim m sordCoe hitor oonteOET Readcedet meetinoTO0%7AThu 28 May 18:30:36R SaveCookiesValuttaksedoaBulk Ed?Whether to retum only results that have boen archivedDescriptiolRootiredRntke Shoe3 турe-t Sand e Get a cupcessful resoonsSend + Visualze response% Send + Write test:Gobr'eVantookttiem...
|
iTerm2
|
NULL
|
NULL
|
87258
|
|
87257
|
IAlSlackFileEditViewGoHistoryWindowHelpActivity Mo IAlSlackFileEditViewGoHistoryWindowHelpActivity MonitorAll ProcessesProcess Name% CPUCPU TimePhpStormkernel_taskWindowServerPostman Helper (Renderer)screenpipereplaydFirefoxCP Isolated Web ContentPostmancoreaudiodClaudeFirefoxPostman Helper (GPU)bluetoothdActivity MonitorFirefoxCP Isolated Web Contentlanguage_server_macos_armFirefoxCP Isolated Web ContentlaunchservicesdFirefoxCP Isolated Web ContentiTerm2Wispr FlowFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentKarabiner-Core-ServiceWispr Flow Helper (Renderer)FirefoxPostman HelperControl Centre128,44:20:52,76125,454,142,724:06:12,078:07:02,096:56,8841,635,73:50:22,516:40:39,3810,541:06,933:48,091:01:52,4329:40,951:49:58,9337,5921:05,629:32,1513:38,2912:00,8458,141:04:25,0116:59,891:05:01,274:24,4120:02,8232:28,3920:52,385:43,3114:27,5955,2020:47,43System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+21,67%41,33%37,00%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana NetsevaEo Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor Stamatov. Steliyan Georgievto VesG. MiraP.. Nikolay Yankov4o James GrahamLukas Kovalik y...#: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:35Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилоример)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87257
|
|
87256
|
IAlSlackFileEditViewGoHistoryWindowActivity Monito IAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskWindowServerPostman Helper (Renderer)screenpipereplaydFirefoxCP Isolated Web ContentPostmancoreaudiodClaudeFirefoxPostman Helper (GPU)bluetoothdActivity MonitorFirefoxCP Isolated Web Contentlanguage_server_macos_armFirefoxCP Isolated Web ContentlaunchservicesdFirefoxCP Isolated Web ContentiTerm2Wispr FlowFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentKarabiner-Core-ServiceWispr Flow Helper (Renderer)FirefoxPostman HelperControl Centre128,4125,454,142,741,635,710,58,45,8HelpCPU Time4:20:52,7624:06:12,078:07:02,096:56,883:50:22,516:40:39,3841:06,933:48,091:01:52,4329:40,951:49:58,9337,5921:05,629:32,1513:38,2912:00,8458,141:04:25,0116:59,891:05:01,274:24,4120:02,8232:28,3920:52,385:43,3114:27,5955,2020:47,43System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp ,52%34,60%38,88%CPUчwHomeDMsActivityFilesLater..•More+ED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana NetsevaEo Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko Kashinski. Galya Dimitrova: Todor Stamatov. Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...#: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:33Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилоример)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87256
|
|
87255
|
PostmanVieWwurcowX HubspotYour team is now on the PostmanVieWwurcowX HubspotYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationProperties > crm/yklorcosrties criact voe> Resd all oropertiesV COLLECYIONSEngsgements› HubspotKera don runttaration nin Sastch He" OaR WeonocoksVeOAuthcrm/v3/properties/(object Type>a groups?-olOoery Namev ut Read all oronartiesee successtul operationenAn error occurred.Rotcteae8 orooey• ЗLАKCHTickarsv Uselupest lilter por comanny /onty ocen dent stnoeywnendademens oo associa.tovoCii anasdsminte did actocitio ou comode.> at get history of property - deal stageut detusersCET SF osuth> est Moatina cutcomes per mectind> cEt Read all properties newCin Randlalll orooartine oct old call dispositionseflist with associationsсET list engagements okert recent enaaaement.cet get dealMiGatemadsmentmiE Docs Params • Authorization Headers 8 Body ScriptsSettinasQuery ParamsKeynapixeyorchiveePakh VarsolesobjectTypoRescanse" Hнom yPATCH httos://api.hubapi.com/engagements/v1/engagements/24927395231patckhttocdllaolhubant.com/crm/v3lob.ecte/meet0os/24927/3952311PATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231ert Uprste desiciandCET OLD doal by idATOUD contaetCMMIDANIENTCSPECS> FLOWSOConndettCoasde em aniooThu 28 May 18:30:32@ SearchCtpendeoeollietmnn sayeCtoe hitory oorteOET ReadCer det meetinoR SaveCookiesValutfaiseMaimeValuemeetingBulk Ed?Whether to retum only results that have boen archivedDescriptionRoctiredRutke Shoe3 турe-") Sand a Geta cupcesstul resoons.2) Send + Visualize respons)% Send + Write test.Giobnts Montt Tookemem...
|
iTerm2
|
NULL
|
NULL
|
87255
|
|
87254
|
SlackFileEditViewGoHistoryWindowActivity MonitorAl SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskreplaydWindowServerscreenpipePostman Helper (Renderer)FirefoxCP Isolated Web ContentcoreaudiodlaunchservicesdbluetoothdActivity MonitorPostmanClaudeFirefoxPostman Helper (GPU)FirefoxCP Isolated Web Contentlanguage_server_macos_armFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr FlowiTerm2SlackWispr Flow Helper (Renderer)Control CentreSlack Helper (Renderer)logdKarabiner-Core-ServiceFirefoxCP Isolated Web Content115,182,558,329,328,024,69,75,33,93,73,33,32,72,72,42,42,32,32,02,01,71,61,51,41,31,21,21,2HelpCPU Time4:20:45,9924:06:05,456:40:37,508:06:59,243:50:20,326:54,6241:06,371:01:52,131:04:24,8621:05,419:31,953:47,6529:40,671:49:58,6837,3616:59,7612:00,6757,9913:38,124:24,311:05:01,1614:05,155:43,2320:47,371:00:44,2617:15,8620:52,3020:02,72System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+38,17%39,85%21,98%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:32Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87254
|
|
87252
|
SlackFileEditViewGoHistoryWindowActivity MonitorAl SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskreplaydWindowServerscreenpipePostman Helper (Renderer)FirefoxCP Isolated Web ContentcoreaudiodlaunchservicesdbluetoothdActivity MonitorPostmanClaudeFirefoxPostman Helper (GPU)FirefoxCP Isolated Web Contentlanguage_server_macos_armFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr FlowiTerm2SlackWispr Flow Helper (Renderer)Control CentreSlack Helper (Renderer)logdKarabiner-Core-ServiceFirefoxCP Isolated Web Content115,182,558,329,328,024,69,75,33,93,73,33,32,72,72,42,42,32,32,02,01,71,61,51,41,31,21,21,2HelpCPU Time4:20:45,9924:06:05,456:40:37,508:06:59,243:50:20,326:54,6241:06,371:01:52,131:04:24,8621:05,419:31,953:47,6529:40,671:49:58,6837,3616:59,7612:00,6757,9913:38,124:24,311:05:01,1614:05,155:43,2320:47,371:00:44,2617:15,8620:52,3020:02,72System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+30,12%31,49%38,39%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:30Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87252
|
|
87251
|
viewwincowX HubspotYour team is now on the Free pl viewwincowX HubspotYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-ony. Yiew team permissiens to see who can edit, or ungrade to restore collaboration.$o7o0sLXThu 28 May 18:30:29@ SearchCtoe hitory oorteCEr RendCr det meetinoPropertios › crm/vs /properties/ obiect Type).OverviewRunShareeV COLLECYIONSEngsgements› HubspotKera don run› Iteration run Search HSOaRWeonocoksVeOAuth~ [ crm/v3/properties/(object Type)› C groups?-olOoey Name› cer Read all properties>rosrereate a property> RESSARCH› SEARCHrekare› rost filter per company / onty open deal stageset engagements old associated oy desl«T engagements old associated by companyCroenisovor dory.oeeCtastnear«T SF oauth&* Meetina cutcomes der meetina> CET Read all properties new> est Read all propertice oldl.atr old call dispositionsCnlliet withaceod thiont«T list engagements oltt recent enaadementeaт get dealerT Get Enanaement (vi)PaтCw https://api.hubapl.com/engagements/v1/engagements/24927395231wranttosdlaothinao.com/cim/walo.scremaatoocnowkoryxtPaтcH https://spi.hubspi.com/engagements/V1/engsgements/24927395231et Uodate deal staacaT OLD deal by idCET OLD conhetCET OLD contact by IDCTOuD commaniasCMMIDANIENTCSPECS> FLOWSOConndettCoasole em anioocrm/v3/properties/{object Type)O You & 13Halo naoola undorarand wour foldar bu add oon dattio ton 6% Mhta vệh AMautooke—tie...
|
iTerm2
|
NULL
|
NULL
|
87251
|
|
87248
|
VieWwurcowX HubspotYour team is now on the Free pl VieWwurcowX HubspotYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationUseful > Contact with all historyhttps://aoi.hubapi.com/contacts/v1/contact/vid/sid/profile?propertyModeavalue.and.historE Docs Params • Authorization • Headers 8 Body Scriots SettingsQuery ParamsKeypropertyModepath VarinbiasThu 28 May 18:30:26© Scarcheiteration run SeatchGET oos historvof eroo ecarkead nlll oro oertitoetoet meetneE) SaveV COLLECYIONSEngsgements› HubspotKera don runttaration nin Sastch HeOaRWeonocoksVeOAuth•RESSARCH1EAKCHSTekareYUeTU› Pest filter per company / only open deal stageset engagements old associated oy dea.eft enanaements dld ascocinted oy comona.Coenisiorvor Kowty.eCinastuenreLT SF oauth› сEт Meeting outcomes per meeting> cT Read all properties new> et Rend all propertioe oldlcet old call dispositionsCniliet with aeeod thioneerust eino se chocnis okut recent enaadementocEt get dealET Got Enanacment (villPATCHhttos://api.hubaoi.com/engagements/v1/enoagements/24927395231wrahttoedlaoihindocom/cim/walooschocaatoo0momkoryPATCH httos://api.hubapi.com/engagements/v1/engagements/24927395231et Uodate dealstaa.CET OLD deal by idCET OLD conthetCET OLD contact by IDCTOuD comnaniasCT OLD company by IDeft Contact with all historyPOST BATCH UPDATE DEWAL FOR TESTINGPOST BATCH UPDATE CONTACT FORTSSTINGNG RATGH LONATE GOMDAMY COP TECTINGWabhookeCMMIDANIENTCSPECS> FLOWSOConndettCoasde em anioovalue.ano-nistoryValte195164413986DescriptiolWco.1oSondCookiesBulk Ed?Bulk Edit .Rescanse" Hнom y" Send & Get a supcesstul resoans.2) Send + Visualize respons% Send + Write test.Globals Vault Toos ,d=g...
|
iTerm2
|
NULL
|
NULL
|
87248
|
|
87247
|
SlackFileEditViewGoHistoryWindowActivity MonitorAl SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskreplaydWindowServerscreenpipeFirefoxCP Isolated Web ContentcoreaudiodPostman Helper (Renderer)syspolicydtccdFirefoxCP Isolated Web ContentbluetoothdFirefoxActivity MonitorlaunchservicesdClaudelanguage_server_macos_armFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr FlowPostmanFirefoxCP Isolated Web ContenttrustdControl CentreFirefoxCP Isolated Web ContentiTerm2Wispr Flow Helper (Renderer)trustd147,1103,653,621,013,99,26,54,24,1HelpCPU Time4:20:39,9324:06:01,116:40:34,438:06:57,703:50:18,8441:05,861:01:51,846:53,3317:28,779:17,594:23,0321:05,221:49:58,549:31,781:04:24,6629:40,5312:00,5516:59,6357,874:24,213:47,4720:02,6615:04,0420:47,2913:38,011:05:01,075:43,155:21,19System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+9,46%23,24%67,31%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor Stamatov. Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:24Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87247
|
|
87246
|
VieWwurcowX HubspotYour team is now on the Free pl VieWwurcowX HubspotYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationUseful › Contact with all historyhttps://aoi.hubapi.com/contacts/v1/contact/vid/sid/profile?propertyModeavalue.and.historE Docs Params • Authorization • Headers 8 Body Scriots SettingsQuery ParamsKeypropertyModepath Varinbies7o0sLXThu 28 May 18:30:12© Scarcheiteration run SeatchGET oos historvof eroo ecarkead nlll oro oertitotoet meetineE) SaveV COLLECYIONSAssociat.ans. ValCws Uhe Kedkeds hpeollecionsamnsnineCOMPAKCContactsCRM Obiec• CRM OwnersCkM PidelinesDeals• Hubspot> Iteration run HSIteration run Scarch HS>Journale webhodake waleAuth• MOoeRiRESSARCH> SEARCHTickotev Useful•Mr titar oar comotavl ooiv doao Canl 401o4deenchoements dd asocito oy oeut endadements dld ascociated ou comoaci> cet get history of property - deal stageCET OCtUROreAT SE ASIth> сt Read all properties new> cy Road all oronertios olalcet old call dispositionscrtlist with aesociation.cer list engagements oldCTTACAOT Anoadementat get dealet Got Enaagement (vllPATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231PaTCHhttos:/api.hubapi.com/engagements/v1/enoaoements/24927395231CTUonAte deslicharCMMIDANIENTCSPECS> FLOWSOlConnnettCoasdle em antooSondCookiesBulk Ed?value.and-nistoryValte195164413986DescriptiolWco.1oBulk Edit .Rescanse" Hнom y") Send a Get a supcesstul resoons.2) Send + Visualize respons)% Send + Write test.Globals Vault Toos ,d=g...
|
iTerm2
|
NULL
|
NULL
|
87246
|
|
87245
|
VieWwurcowX HubspotwYour team is now on the Free p VieWwurcowX HubspotwYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationUseful › Contact with all historyhttps://aoi.hubapi.com/contacts/v1/contact/vid/sid/profile?propertyModeavalue.and.historE Docs Params • Authorization • Headers 8 Body Scriots SettingsQuery ParamsKeypropertyModepath VarinbiesV COLLECYIONSAssociat.ans. ValCwsUheKedkreds np CollecioncamnsninsCOMPAKCContactsCRM Obiec• CRM OwnersCkM PidelinesDeals• Hubspot> Iteration run HSIteration run Scarch HSJournale webhoddkewneAuth• МОРКRESSARCH> SEARCHTickotev Useful•Mr titar oar comotavl ooiv doao Canl 401o4dct engagements old associated by dealut endadements dld ascociated ou comoaci> cet get history of property - deal stageCET OCtUROreAT SE ASIth> сT Read all properties new> ct Road all oronertios olalcet old call dispositionscrtliet with aesociationscer list engagements oldCnrecaot ancadementat get dealet Got Enaagement (vllPATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231PaTCHhttos:/api.hubapi.com/engagements/v1/enoaoements/24927395231CTUonAte deslicharCNMIDANNENTeSPECS> FLOWSOlConnnettCoasdle em antoovalue..and histon1061AA41306ARescanse" Hнom y7o0sLXThu 28 May 18:30:10© Scarchleliteration msCt det titoryPOSt freroerodET Read allorooetdet meetDescriotionDescriptionNo emaronmentyB) SaveSharsCookiesBulk EditanButk EditVariables in requeso tokenAll variabies195164413986CPmpgfrmMxIZQINQMIBkQEwrAgwACA)Send y Gat a succosstul resoons.Send + Visualize response* Send + Write tostsGlobals Vault Toos ,d#&...
|
iTerm2
|
NULL
|
NULL
|
87245
|
|
87244
|
VieWwurcowX HubspotYour team is now on the Free pl VieWwurcowX HubspotYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationUseful › Contact with all historyhttps://aoi.hubapi.com/contacts/v1/contact/vid/sid/profile?propertyModeavalue.and.historE Docs Params • Authorization • Headers 8 Body Scriots SettingsQuery ParamsKeypropertyModepath VarinbiesTeRetannuasV COLLECYIONSAssociat ans.ValCws Uhe Kedkeds hpeollecionsamnsnineCOMPAKCContactsCRM Obiec• CRM OwnersCkM PidelinesDeals• Hubspot> Iteration run HSIteration run Scarch HSJournale webhoddkewneAuth• МОРКRESSARCH> SEARCHTickotev Useful•Mr titar oar comotavl ooiv doao Canl 401o4deenchoements dd asocito oy oeut endadements dld ascociated ou comoaci> cet get history of property - deal stageCET OCtUROreAT SE ASIth> сT Read all properties new> cy Road all oronertios olalcet old call dispositionscrtliet with aesociationscet list engagements oldCnrecaot ancadementat get dealet Got Enaagement (vllPATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231PaTCHhttos:/api.hubapi.com/engagements/v1/enoaoements/24927395231CTUonAte deslicharCMMIDANIENTCSPECS> FLOWSOlConnnettCoasdle em antoovalue..and histon1061AA41306ARescanse" Hнom y7o0sLXThu 28 May 18:30:07scarchleliteration msCet det titeyaPOSt freroerodET Read allorooet det meetrDescriotionDescriptionNo ematonmentvB) SaveShareCookiesBulk EditanButk EditVariables in requesc tokenAlvannoles195164413986)Send + Got a succosstul resoons.Send + Visualize response* Send + Write tost.Globals Vault Toos ,d#&...
|
iTerm2
|
NULL
|
NULL
|
87244
|
|
87243
|
IAlSlackFileEditViewGoHistoryWindowActivity Monito IAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUkernel_taskPhpStormreplaydWindowServerPostman Helper (Renderer)deletedscreenpipelanguage_server_macos_armtrialdFirefoxCP Isolated Web ContentPostmanPostman Helper (GPU)clouddFirefoxCP Isolated Web ContentcoreaudiodbackupdClaudebluetoothdActivity MonitorFirefoxmobileassetdtccdtccdFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentidleassetsdWispr FlowiTerm2131,8111,254,854,249,629,128,813,09,39,18,47,16,86,46,25,74,84,13,13,03,02,92,32,22,11,91,91,7HelpCPU Time24:05:36,914:20:10,486:40:23,168:06:49,736:50,1314,893:50:13,3411:59,957,3641:03,743:46,5736,587,1732:27,951:01:50,611:14,8229:39,7321:04,439:31,051:49:57,8119,839:17,356:43,0557,4316:59,080,654:23,811:05:00,68System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+33,56%35,48%30,97%CPUED→Jiminny...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:03Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87243
|
|
87242
|
4oVieWwurcowX HubspotYour team is now on the Free 4oVieWwurcowX HubspotYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationUseful › Contact with all historyhttps://aoi.hubapi.com/contacts/v1/contact/vid/sid/profile?propertyModeavalue.and.historE Docs Params • Authorization • Headers 8 Body Scriots SettingsQuery ParamsKeypropertyModepath Varinbies7o0sLXThu 28 May 18:30:03scarchleliteration msCt det titoryPOSt freroerodET Read allorooet det meetr$X A1V COLLECYIONSAssociat.ans. ValCwsUheKedkreds np CollecionsamnsnineCOMPAKCContactsCRM Obiec• CRM OwnersCkM PidelinesDeals• Hubspot> Iteration run HSIteration run Scarch HSJournale webhoddkewneAuth• МОРКRESSARCH> SEARCHTickotev Useful•Mr titar oar comotavl ooiv doao Canl 401o4dct engagements old associated by dealut endadements dld ascociated ou comoaci> cet get history of property - deal stageCET OCtUROreAT SE ASIth> сT Read all properties new> ct Road all oronertios olalcet old call dispositionscrtliet with aesociationscer list engagements oldCnrecaot ancadementat get dealet Got Enaagement (vllPATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231PaTCHhttos:/api.hubapi.com/engagements/v1/enoaoements/24927395231CTUonAte deslicharCNMIDANNENTeSPECS> FLOWSOlConnnettCoasdle em antoovalue..and histon1061AA41306ADescriotionDescriptionNo emaronmentyB) SaveSharsCookiesBulk EditButk EditVariables in requeso tokenAll variabies195164413986COPE6dTKMxIZQINQMI8KQEwrAgwACARescanse" Hнom y)Send y Gat a succosstul resoons.Send + Visualize response* Send + Write tost.Globals Vault Toos ,d#&...
|
iTerm2
|
NULL
|
NULL
|
87242
|
|
87241
|
IAlSlackFileEditViewGoHistoryWindowActivity Monito IAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUkernel_taskPhpStormreplaydWindowServerPostman Helper (Renderer)deletedscreenpipelanguage_server_macos_armtrialdFirefoxCP Isolated Web ContentPostmanPostman Helper (GPU)clouddFirefoxCP Isolated Web ContentcoreaudiodbackupdClaudebluetoothdActivity MonitorFirefoxmobileassetdtccdtccdFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentidleassetsdWispr FlowiTerm2131,8111,254,854,249,629,128,813,09,39,18,47,16,86,46,25,74,84,13,13,03,02,92,32,22,11,91,91,7HelpCPU Time24:05:36,914:20:10,486:40:23,168:06:49,736:50,1314,893:50:13,3411:59,957,3641:03,743:46,5736,587,1732:27,951:01:50,611:14,8229:39,7321:04,439:31,051:49:57,8119,839:17,356:43,0557,4316:59,080,654:23,811:05:00,68System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+41,74%46,55%11,72%CPU:ED→Jiminny...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messages(L. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan GeorgievLo Ves8. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:30:02Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87241
|
|
87240
|
4oVieWwurcowX HubspotwYour team is now on the Free 4oVieWwurcowX HubspotwYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissicas to see who can edit, or ungrade to restore collaborationUseful › Contact with all historyhttps://aoi.hubapi.com/contacts/v1/contact/vid/sid/profile?propertyModeavalue.and.historE Docs Params • Authorization • Headers 8 Body Scriots SettingsQuery ParamsKeypropertyModepath VarinbiesThu 28 May 18:30:02© Scarchleliteration msCet det titeyaPOSt freroerodET Read allorooet det meetrV COLLECYIONSAssociat ans.ValCwsUheKedkreds np CollecioncamnsninsCOMPAKCContactsCRM Obiec• CRM OwnersCkM PidelinesDeals• Hubspot> Iteration run HSIteration run Scarch HSJournale webhoddkewneAuth• МОРКRESSARCH> SEARCHTickotev Useful•Mr titar oar comotavl ooiv doao Canl 401o4dct engagements old associated by dealut endadements dld ascociated ou comoaci> cet get history of property - deal stageCET OCtUROreAT SE ASIth> сT Read all properties new> ct Road all oronertios olalcet old call dispositionscrtliet with aesociationscer list engagements oldCnrecaot ancadementat get dealet Got Enaagement (vllPATCи httos://api.hubapi.com/engagements/V1/engagements/24927395231PaTCHhttos:/api.hubapi.com/engagements/v1/enoaoements/24927395231CTUonAte deslicharCMMIDANIENTCSPECS> FLOWSOlConnnettCoasdle em antoovalue..and histon1061AA41306ADescriotionDescriptionNo ematonment vR Swv.SharsCookiesBulk EditButk EditOe AINow ChaInfoRescanse" Hнom y)Send y Gat a succosstul resoons.Send + Visualize response* Send + Write tostsO AutosGiobnts Moult Tookemem...
|
iTerm2
|
NULL
|
NULL
|
87240
|
|
87239
|
SlackFileEditViewGoHistoryWindowActivity MonitorAl SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUkernel_taskPhpStormreplaydWindowServerscreenpipedeletedtrialdFirefoxCP Isolated Web ContentcoreaudiodPostman Helper (Renderer)Activity MonitorWispr Flow Helper (Renderer)bluetoothdSlack Helper (Renderer)FirefoxWispr FlowPostmanlanguage_server_macos_armlaunchservicesdfileproviderdClaudemobileassetdFirefoxCP Isolated Web ContentWispr Flow Helper (GPU)tccdFirefoxCP Isolated Web ContentFirefoxCP Isolated Web Contentsyspolicyd159,3146,166,460,860,645,39,99,67,17,04,64,64,24,23,93,93,93,73,53,53,23,03,03,02,82,72,62,4HelpCPU Time24:05:29,884:20:04,546:40:20,248:06:46,833:50:11,8013,346,8641:03,261:01:50,286:47,489:30,8910,0821:04,211:00:43,791:49:57,651:38,933:46,1211:59,251:04:23,551:22,7229:39,4719,6757,3210,989:17,2016:58,9710:20,5717:28,46System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+37,71%42,71%19,58%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor Stamatov. Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:57Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87239
|
|
87238
|
SlackFileEditViewGoHistoryWindowActivity MonitorAl SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUkernel_taskPhpStormreplaydWindowServerscreenpipedeletedtrialdFirefoxCP Isolated Web ContentcoreaudiodPostman Helper (Renderer)Activity MonitorWispr Flow Helper (Renderer)bluetoothdSlack Helper (Renderer)FirefoxWispr FlowPostmanlanguage_server_macos_armlaunchservicesdfileproviderdClaudemobileassetdFirefoxCP Isolated Web ContentWispr Flow Helper (GPU)tccdFirefoxCP Isolated Web ContentFirefoxCP Isolated Web Contentsyspolicyd159,3146,166,460,860,645,39,99,67,17,04,64,64,24,23,93,93,93,73,53,53,23,03,03,02,82,72,62,4HelpCPU Time24:05:29,884:20:04,546:40:20,248:06:46,833:50:11,8013,346,8641:03,261:01:50,286:47,489:30,8910,0821:04,211:00:43,791:49:57,651:38,933:46,1211:59,251:04:23,551:22,7229:39,4719,6757,3210,989:17,2016:58,9710:20,5717:28,46System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+42,23%39,67%18,10%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan Tomovo Petko KashinskiGalya Dimitrova: Todor Stamatov. Steliyan Georgievto VesG. MiraP.. Nikolay Yankov4o James GrahamLukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:56Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е сьс статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87238
|
|
87237
|
Postmanviewwurcowe Salestorce100% 142Thu 28 May 18 Postmanviewwurcowe Salestorce100% 142Thu 28 May 18:29:56@ SearchGET QUERCet oft obitct deleteoet(url)"services/data.,SalesforcevФ*AIR SIvev COLLECTIONSamazon connest cooANCV SOROrCh> CUI> E USEFUL> NotesCOUE:un SGApGHWAGorC soO-DudoGET Salesforce Get RecordaT Salesforce Get Record Duobcateet Salacforca Grasta RacocdlM ColncatAo Got CuetAm Sioh MotnntCET SSlASTOrCe SOOIGET Salesforce SOQL DuplicateaT Salestorce Sool Duolicate 12)CET Salesforce SOSLobied descnorMTCTINHOtO AhiOACT ae orecass eatedory queryast llef nellVesrvicoe datahSo Ohmselmat OrcanizationCET Get Organization IDCacobicd deleteatCET Object Permissionsct custom tieloSalociotturi enmreetotsh dinnortnneieetMumhar dt ChlaenaolnE Docs Params aQuery ParameSettinas,ValudDescriptiorBulk EditSELECT - *Id. - •Name, - •Number_of_Salespeoplec, ~ •Website, • *Phone, - •BilingStreet, - •..Response"0 HatorySeod + (cat a successiul resooasaSend + Visualize response* Send + Write testsCNMIDANNENTeSPECS> FLOWSEe connnct GarF Consoln5 Termioat0 0 AdO Auto vGiobnts Moult Tookemem...
|
iTerm2
|
NULL
|
NULL
|
87237
|
|
87236
|
Postmanviewwurcowe Salestorce100% 142Thu 28 May 18 Postmanviewwurcowe Salestorce100% 142Thu 28 May 18:29:54@ SearchGET QUERCet oft obitct deleteoet(url)"services/data.,SalesforcevФ*AIR SIvev COLLECTIONSamazon connest cooANCV SOROrCh> CUI> E USEFUL> NotesCOUE:un SGApGHWAGorC soO-DudoGET Salesforce Get RecordaT Salesforce Get Record Duobcateet Salacforca Grasta RacocdlM ColncatAo Got CuetAm Sioh MotnntCET SSlASTOrCe SOOIGET Salesforce SOQL DuplicateaT Salestorce Sool Duolicate 12)CET Salesforce SOSLobied descnorMTCTINHOtO AhiOACT ae orecass eatedory queryast llef nellVesrvicoe datahSo Ohmselmat OrcanizationCET Get Organization IDCacobicd deleteatCET Object Permissionsct custom tieloSalociotturi enmreetotsh dinnortnneieetMumhar dt ChlaenaolnE Docs Params aQuery ParameSettinas,ValudDescriptiorBulk EditSELECT - *Id. - •Name, - •Number_of_Salespeoplec, ~ •Website, • *Phone, - •BilingStreet, - •..Response"0 HatorySeod + (cat a successiul resooasaSend + Visualize response# Send + Write testsCNMIDANNENTeSPECS> FLOWSEe connnct GarF Consoln5 Termioat0 0 AdO AutosGiobnts Moult Tookemem...
|
iTerm2
|
NULL
|
NULL
|
87236
|
|
87235
|
IAlSlackFileEditViewGoHistoryActivity MonitorAll P IAlSlackFileEditViewGoHistoryActivity MonitorAll ProcessesProcess NamePhpStormkernel_taskreplaydWindowServerlogdFirefoxCP Isolated Web ContentscreenpipeFirefoxcoreaudiodbackupddeletedbluetoothdlaunchservicesdmobileassetdActivity MonitorFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentfairplaydWispr Flowlanguage_server_macos_armFirefoxCP Isolated Web ContentClaudeFirefoxCP Isolated Web ContenttccdWispr FlowFirefoxCP Isolated Web ContentanedtrustdWindow% CPU146,1100,755,324,623,98,57,87,65,64,84,64,03,83,73,22,82,42,42,32,32,22,01,91,81,81,81,71,7$1HelpCPU Time4:19:56,7624:05:21,406:40:16,708:06:43,6017:14,3341:02,743:50:08,581:49:57,441:01:49,901:14,5210,9321:03,991:04:23,3619,519:30,6416:58,8213:37,607,984:23,6011:59,0525:32,8429:39,3057,169:17,051:38,7220:02,1124,405:21,06System:Threads CPUMemoryEnelIdle Wake-UpsKp .07%CPUHomeDMsActivityFilesLaterMorePostmanED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messages(L. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovLo Petko Kashinski. Galya Dimitrova: Todor StamatovRo Steliyan Georgievto VesG. Mira. Nikolay YankovJames GrahamLukac Kovalik(alo)100% <78• Thu 28 May 18:29:52Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилоример)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage llivana NetsevalPS...
|
iTerm2
|
NULL
|
NULL
|
87235
|
|
87234
|
VieWwurcowPwovscors#12121 on JY-20963-fx-lAdtwycon VieWwurcowPwovscors#12121 on JY-20963-fx-lAdtwycontroller.pngCSamviceTesconrMEINSTALLMOUmacurayscrict.on© RecordSelector.phgO ActMily prM.INTERNALWEBHOOK SETU? maliminny storaceOpportunitySyncTrait.php© CrmEntityRepository.phgclass service excenos baseservace olesentsMalicenses.mapubuzc tunccion anooristades tarray suypes & nult, tsurane sssangscagenane & nulle escadeMakere0 package-lock.jsonphpstan.neon.distF phpstan-baseline.neonphpunit.xmlraw.sol query.saMAesNeiMs ma4484494504S4i sonar-project propertiesEtest.py<> Untitied Diagram.xmlus vetur.config.isMAWISAHOeK SinEoiNGMOSUNnt. Sytemallloranesv=0 Scratches and Consolesv @ Database ConsolesV AEU# console cUA DEAL RISKS (EUmeutsunSarvicaei+oc|exvontameAA console 1 $ 650 mgui users 1 $ 548 msy2liminny@localhostA HSJJocalA SPV A PROD# ConcalaV A STAGINGH Concal"noakor"tean_1dnane=> Sthis->tean->1d,=> ab_strinuidth(Ssl"label"),'Label"'type=> ab_strinuidth(Ss['label')=> Stage::TYPE_OPPORTUNITY,seauence"=> $s["display0rder'),'is_selectable' => $s['active')proodozersy=> $s['probability") * 100Siorto,width: 50)width: 191)D:if (SnissingStageNane === Ssi"id') 4DuanostooetodehustacesudostacrosSousiness?rocess-sstaces@-svnclsstages.r=custom.loglaravei.io"A SF jiminny@localhostHSJocal jiminny@localhostA console (PRODasaimlorce/Service.onA console (EU) X uin users (EU# console [STAGINGDo liminny v031 A9 A29 V 3 Y 109 A Vselect * from text relays where created at > 2826-85-01*÷Seloe* o acewioes oroer oyooeseSELE * FROMopportunItles WHERE UUTCTO_OIN 84a9c.ad-2087-4453-9072-28aeb/8ccf80CONCAT(U.1d, CASE WHEN U.1d = t.owner_1d THEN" (ouner)" ELSE" END) AS USer_1d,sa.*townen o sror social accounts seJOIN users u on u.id = sa.sociable.1ohiTihtassetAHERE U.team_id = 555 and sa.provider = 'hubspot":HIДЕI ДВHTTO0У L7inu co moy toitsiomeesdales Orcnnworeebeeionhnves ohtine oDon+0.aPetooete60orobabuty" s Tloat (icatalcetadata"orobabto tym)hThis s the root causeHere's the full explanationRoot eause:moorStages overwmes D8 probabllty with Huosoots valueThe flow is1. Someone manually changed stage 20616's probability in the Jiminny DB to 100.00 on 2826-45-44. The stage crn provider 1d: "elosedtost" k2. ImportStages() runs (during any pipeline/stage sync), It callsService.php:445-454Sstage = Sthis->config-ostages()-withTrashed()->update0rCreate(ferprowderesto cetorosbliewr1+ CCooeS? Adaotiv7 Outpurjiminny.opportunitiesIID fiminny stages(ID Result 12 xTtaw w400E6user idMlenailDidsociable_1cprovider user idi provider user tokenprovider refresh-tokenEeYos nes.innatnach token ayasaneB provideri stateauthscopeMretry aftenM ccested.atupdated at18826 (oaner)alex-nogunhtcoontewecroont.con385911892477027227)CPnogfionNx:201NOM8KOFunA0uACAKUAhTWBBZRA0F0Bx.7sO0M/WuothYo1K-CMhSF070wJpK22n|170617012u6097A-Toy01 MOM1 8K0FunAFUACBkGawFEThwBAR7BAOFFATEFAOFBAOFBAOFBAOUBF@9BAOFBAY1.CFEpRVL0P2.0/RhNQcP4bQtvZyBUISoM-IIESAFoAYABom65/2FnAAnAeu1-8633-482c-4614-aae8-S88da6aß92ehconnected<null)2825-4-18 14:8841132ankoaCrhe16:12.2provider user token encryptedIeyJpd¡[6T?NXeFR"bT2RbG1yZUE2cVpQQnE4aẞc9PSIsInZhbHVLẞoÎdktIMnZ2UGY3L8t423E1TnVJNkxD0U9XWTRTSnVDCmhßKz©2Uk5HThmYU9гbkM22HFvSИRNUL?MRG1·M2ttRU43SHRqRHJRVStvYßdKK3FnUFFWFhpV21GWnIvVHRVZz840kxXMG1YcBRSaHZnY304U*L2VVBDSTDprovider refresh token encryptedleyJod$t6InMcUHh6Z2F. 24BxUF2TOHN3ZEtEWWC9PSIsInZhbHVLTo1aUs5L2BXWW9GT2dcZBdhd102a105SMocN1B1eHFCaTK5TENnVnNPV2h0YnF0b1U3eWV=YmscU0hnV2VKCEp2dCTST@1hYy26ImU3NTNSN5gcNnTxNWE1M2NkND112WNmMGUSY2B&YnYSOTBÄYTZmZGFmMmYy2TY321Mlencnyattion key (Hex)LBx SA75290E40RERE0O2REEGE:CBAERZ0SQ.ARZPATR.7/.601283254521R40597BAB1232AA0295315E / ллл00174052LITS 9...
|
iTerm2
|
NULL
|
NULL
|
87234
|
|
87233
|
IAlSlackFileEditViewGoHistoryWindowActivity Monito IAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUkernel_taskPhpStormreplayddeletedscreenpipeWindowServermdsmds_storesFirefoxCP Isolated Web ContentclouddtrialdlaunchdcoreaudiodFirefoxCP Isolated Web ContentlaunchservicesdlogdbackupdActivity MonitorbluetoothdtccdFirefoxlanguage_server__macos_armSlack Helper (Renderer)tccdFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr FlowSlack151,6148,549,039,137,124,516,212,99,68,58,36,66,15,75,64,84,64,64,03,83,12,82,82,62,42,42,22,1HelpCPU Time24:05:16,294:19:49,356:40:13,9010,703:50:08,188:06:42,3532:15,231:10:32,5041:02,316,716,3119:35,651:01:49,6213:37,481:04:23,1717:13,111:14,279:30,4821:03,789:16,961:49:57,0511:58,941:00:43,526:42,8516:58,6857,064:23,4814:04,68System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+38,49%27,37%34,14%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messages(L. Iliyana NetsevaEo Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto VesG. MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:50Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87233
|
|
87232
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// 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
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// 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();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity tr...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87232
|
|
87231
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// 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
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// 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();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity tr...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87231
|
|
87230
|
VieWwurcowPwovscors#12121 on JY-20963-fx-lAdtwycon VieWwurcowPwovscors#12121 on JY-20963-fx-lAdtwycontroller.pngCSamviceTesconrMEINSTALLMOUmacurayscrict.on© RecordSelector.phgM.INTERNALWEBHOOK SETU? maliminny storaceOpportunitySyncTrait.php© CrmEntityRepository.phgclass service excenos baseservace olesentsMalicenses.mapubuzc tunccion anooristades tarray suypes & nult, tsurane sssangscagenane & nulle escadeMakere0 package-lock.jsonphpstan.neon.distF phpstan-baseline.neonphpunit.xmlraw.sol query.saMAesNeiMs ma4484494504S4i sonar-project propertiesEtest.py<> Untitied Diagram.xmlus vetur.config.isMAWISAHOeK SinEoiNGMOSUNnt. Sytemallloranesv =0 Cemtches and Consolosv @ Database ConsolesvAtU# console cUA DEAL RISKS (EU3meutsunSarvicaeivontameAA console 1 $ 650 mgui users 1 $ 548 msy2liminny@localhostA HSJJocalA SPV A PROD# ConcalaV A STAGINGH Conenld"noakor"tean_1dnane=> Sthis->tean->1d,=> ab_strinuidth(Ssl"label"),'Label"'type=> ab_strinuidth(Ss['label')=> Stage::TYPE_OPPORTUNITY,seauence"=> $s["display0rder'),'is_selectable' => $s['active')proodozersy=> $s['probability") * 100width: 50)width: 191)D:if (SnissingStageNane === Ssi"id') 4DuanostooetodehustacesudostacrosSousiness?rocess-sstaces@-svnclsstages.r=custom.loglaravel.lodA SF jiminny@localhostHSJocal jiminny@localhostA console (PRODasaimlorce/Service.onA console (EU) X uin users (EU# console [STAGINGOojiminny031 A9 A29 Y 3 109 A Vselect * from text relays where created at > 2826-85-01*÷Seloe* o acewioes oroer oyooeseSELE * FROMopportunItles WHERE UUTCTO_OIN 84a9c.ad-2087-4453-9072-28aeb/8ccf80CONCAT(U.1d, CASE WHEN U.1d = t.owner_1d THEN" (ouner)" ELSE" END) AS USer_1d,sa.*townen o sror social accounts seJOIN users u on u.id = sa.sociable.1ohei teane+AHERE U.team_id = 555 and sa.provider = 'hubspot":TO0У L7Inu cowoy 1oc3ialmeesdales Orcnnworeeoeionhnves dohtine Oooond+0.Pitocottoe nhcoo..60orobabuty" s Tloat (icatalcetadata"orobabto tym)hThis s the root causeRoot eause:moorStages overwmes D8 probabllty with Huosoots valueThe flow is1. Someone manually changed stage 20616's probability in the Jiminny DB to 100.00 on 2826-45-44. The stage crn provider 1d: "elosedtost" k2. ImportStages() runs (during any pipeline/stage sync), It callsservice.php:445-454Sstage = Sthis->config-ostages()-withTrashed()->update0rCreate(ferprowderesto cetorosbliewr1+ CCooeS? Adaotiv7 Outpurjiminny.opportunitiesIID fiminny stages(ID Result 12 x1rowvOu400E6user idMlenailDidsociable_1cprovider user idprovider user tokenprovider refresh-tokenEeYos nes.innatnach token ayasaneB provideri stateauthscopeMretry aftenM ccested.atupdated at18026 (oaner)Aley-nogunhtcoont elwecreertcon3859111892447827227ICPnogfinnNx1201NOMBKOF.DAO.ACAKUAN URBARA0FORxi7SO0M/WwothYo1K.CMhSF070.PKPCn0/7061Z0142a6097ANTOy01NOM8KOF.0AFUACBk6awFFThwBAR7BAOFFATEEAOFBAOFRAOFBAOURFGOBAOEBAYI.CEFPRV1OP2GiARhN0CP4DPtvZVBUTSoM-IIESAFOAYABom65/2FnAAnAFeu1-8633-482c-4614-aae8-S88da6aß92ehconnectedsnulta<null)2825-4-18 14:884118292k090.284:2.2provider user token encryptedIeyJod#[6T?NXeFR™bT2RbG1yZUE2cVpQQnE4aẞc9PSIsInZhbHVLẞoÏdktIMnZ2UGY3L8t423E1TnVJNkxDDU9XWTRTSnVDCmhBKz©2Uk5HThmYU9гbkM22HFvSWRNUL?MRG1·M2ttRU43SHRoRHJRVStvYßdKK3FnUFFWFhpV21GWnIvVHRVZz840kxXMG1YcBRSaHZnY304U*L2VVBDSTDprovider refresh token encryptedleyJod$t6InMcUHh6Z2F.74BxUF2TOHN3ZEtEWWC9PSIsInZhbHMLToiaUs5L2BXWW9GT2dcZBdhd102a105SMocN1B1eHFCaTK5TENnVnNPV2h0YnF0b1U3eWV=YmsrU0hnV2VKCEp2dCTST@1hYyZ6ImU3NTNSN5gcNnTxNWE1M2NkND142WNmMGUSY2B&YnYSOTBÄYTZmZGFmMmYy2TY321Mlencnyattion key (Hex)LBx040203907822.773677745265E0095SA75700E4ORERERO2REERE.CBAERZOSQ.ARLPJTR.7/.601282254521R20597BAB1232AA029534SE9099996EXA4A9/092A86/8065700010796/ CSV.TTTO0...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87230
|
|
87229
|
SlackFileEditViewGoHistoryWindowHelpActivity Monit SlackFileEditViewGoHistoryWindowHelpActivity MonitorAll ProcessesProcess Name% CPUCPU TimePhpStorm196,54:19:33,73kernel_taskreplayd126,055,024:05:03,46WindowServermdsFirefoxCP Isolated Web ContentscreenpipetrialdclouddbackupdcoreaudiodamsengagementdlaunchservicesdlaunchdbluetoothdtccdFirefoxFirefoxCP Isolated Web ContentmobileassetdActivity Monitormds_storesClaudetccdlanguage_server_macos_armFirefoxCP Isolated Web ContentlogdFirefoxCP Isolated Web Contentdeleted6:40:09,038:06:39,9332:12,2941:01,323:50:05,445,876,261:13,971:01:49,041,211:04:22,8619:35,2721:03,409:16,751:49:56,7324:10,6319,329:30,081:10:31,5029:39,036:42,7011:58,6743:53,6517:11,8316:58,427,07System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+29,08%35,94%34,99%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messages(L. lliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto Ves% MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:40Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87229
|
|
87228
|
SlackFileEditViewGoHistoryWindowHelpActivity Monit SlackFileEditViewGoHistoryWindowHelpActivity MonitorAll ProcessesProcess Name% CPUCPU TimePhpStorm196,54:19:33,73kernel_task126,024:05:03,46replayd55,0WindowServermdsFirefoxCP Isolated Web ContentscreenpipetrialdclouddbackupdcoreaudiodamsengagementdlaunchservicesdlaunchdbluetoothdtccdFirefoxFirefoxCP Isolated Web ContentmobileassetdActivity Monitormds_storesClaudetccdlanguage_server_macos_armFirefoxCP Isolated Web ContentlogdFirefoxCP Isolated Web Contentdeleted6:40:09,038:06:39,9332:12,2941:01,323:50:05,445,876,261:13,971:01:49,041,211:04:22,8619:35,2721:03,409:16,751:49:56,7324:10,6319,329:30,081:10:31,5029:39,036:42,7011:58,6743:53,6517:11,8316:58,427,07System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp ,98%24,67%53,35%HomeDMsActivityFilesLater..•More+ED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messages(L. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto Ves% MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:37Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87228
|
|
87227
|
IAlSlackFileEditViewGoHistoryWindowActivity Monito IAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskscreenpipereplaydWindowServerdeletedFirefoxCP Isolated Web ContentlaunchservicesdcoreaudiodActivity Monitorbluetoothdlanguage_server_macos_armSlackFirefoxCP Isolated Web ContentjavaWispr FlowFirefoxCP Isolated Web ContentBitwardenlaunchdlogdtccdWispr FlowSlack Helper (Renderer)Wispr Flow Helper (Renderer)Notion Helper (Renderer)ClaudeiTerm2Karabiner-Core-Service345,2177,581,238,432,116,09,88,87,43,83,63,12,82,82,72,72,31,81,71,71,71,61,61,61,51,51,41,2HelpCPU Time4:19:16,8724:04:49,303:50:03,656:40:03,438:06:36,855,7641:00,341:04:22,411:01:48,429:29,7321:03,0011:58,3814:04,4456,626,494:23,0516:58,172:31,7219:35,0017:10,689:16,541:38,461:00:43,145:42,2725:15,6029:38,741:05:00,0320:51,48System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+29,92%30,28%39,80%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan Georgievto Ves% MiraP.. Nikolay Yankovdo James GrahamE Lukas Kovalik y...#: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:28Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87227
|
|
87226
|
UIAlSlackFileEditViewGoHistoryWindowActivity Monit UIAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskscreenpipereplaydWindowServerdeletedFirefoxCP Isolated Web ContentlaunchservicesdcoreaudiodActivity Monitorbluetoothd3PSBitwardenlaunchdlogdtccdWispr FlowSlack Helper (Renderer)Wispr Flow Helper (Renderer)Notion Helper (Renderer)ClaudeiTerm2Karabiner-Core-Service345,2177,581,238,432,116,09,88,87,43,83,61,81,71,71,71,61,61,61,51,51,41,2HelpCPU Time4:19:16,8724:04:49,303:50:03,656:40:03,438:06:36,855,7641:00,341:04:22,411:01:48,429:29,7321:03,002:31,7219:35,0017:10,689:16,541:38,461:00:43,145:42,2725:15,6029:38,741:05:00,0320:51,48System:User:Idle:Threads26355965322035221114$14446482321216616CPUMemoryIdle Wake-Ups EnelKDHomeDMsActivity+39,79%60,09%0,12%CPUED→Jiminny ...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# supportNF8. Stefka Stoyanovao Stoyan TomovEo Petko Kashinski. Galya Dimitrova: Todor Stamatov&. Steliyan GeorgievLo Ves% Mira. Nikolay YankovdoJames GrahamE Lukas Kovalik y...::: AppsJira Cloud100% <78• Thu 28 May 18:29:25QDescribe what you are looking for• lliyana Netseva• MessagesAdd canvas+една заявка (Asked hиилоример)Todayтука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsI.leae Vavalile c.ea neePostmanLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
iTerm2
|
NULL
|
NULL
|
87226
|
|
87225
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lMEINSTALLMOUmacurayscrict.onM.INTERNALWEBHOOKSEUP maliminny storaceMalicenses.maMakere0 package-lock.jsonphpstan.neon.distF phpstan-baseline.neon4>phpunit.xmlraw.sol query.saMAesNeiMs ma4484494504S4i sonar-project propertiesEtest.py<> Untitied Diagram.xmlus vetur.config.isMAWISAHOeK SinEoiNGMOSUNnt. Sytemallloranesv =0 Cemtches and Consolosv @ Database ConsolesvAtU# console cUA DEAL RISKS (EUmeutsunSarvicaei+oc|exvontameAA console 1 $ 650 mgui users 1 $ 548 msy2liminny@localhostA HSJJocalA SFV A PROD# ConcalaV A STAGINGH Conenld"noakorServiceTestCSamviceTesconr© RecordSelector.phgOpportunitySyncTrait.php© CrmEntityRepository.phgclass service excenos baseservace olesentspubuzc tunccion anooristades tarray suypes & nult, tsurane sssangscagenane & nulle escade"tean_1dnane=> Sthis->tean->1d,=> ab_strinuidth(Ssl"label"),'Label"'type=> ab_strinuidth(Ss['label')=> Stage::TYPE_OPPORTUNITY,seauence"=> $s["display0rder'),'is_selectable' => $s['active")proodozersy=> $s['probability") * 100Siorto,width: 50)width: 191)D:if (SnissingStageNane === $s["id')) 4DuanostooetodehustacesudostacrosSousiness?rocess-sstaces@-svnclsstages.r=custom.loglaravei.io"A SF jiminny@localhostHSJocal jiminny@localhostA console (PRODA console (EU) X uin users (EU# console [STAGINGDo liminny v031 A9 A29 У 3 Y109 A Vselect * from text relays where created at > 2826-85-01*÷SPloe*on acewoes oroer oyooeSeSELE * FROMopportunItles WHERE UUTCTO_OIN 84a9c.ad-2087-4453-9072-28aeb/8ccf80CONCAT(U.1d, CASE WHEN U.1d = t.owner_1d THEN" (ouner)" ELSE" END) AS USer_1d,sa.*"ownen so sr0r social accounts seJOIN users u on u.id = sa.sociable.1ohiTihtassetuncosonteressAHERE U.team_id = 553 and sa.provider = 'hubspot":oo nu comoy to:tgiet+0.60HIДЕI ДВHTmeesdales Orcnnworeeoeionhnves dohtine OooondRitoootaNorenhooer.orobabuty" s Tloat (icatalcetadata"orobabto tym)hThis s the root causeHere's the full explanationRoot eause:moorStages overwmes D8 probabllty with Huosoots valueThe flow is1. Someone manually changed stage 20616's probability in the Jiminny DB to 100.00 on 2826-45-44. The stage crn provider 1d: "elosedtost" k2. ImportStages() runs (during any pipeline/stage sync), It callsservice.php:445-454Sstage = Sthis->config-ostages()-withTrashed()->update0rCreate(ferorowideresaeo cetorosbliewr1+ CCooeS, Adaotive7 Outpurjiminny.opportunitiesIID fiminny stages(ID Result 12 x1rowvOu400E6user idMlenailDidsociable_1cprovider user idprovider user tokenprovider refresh-tokenEeYos nes.innatnach token ayasaneB provideri stateauthscopeMretry aftenM ccested.atB updated.at18026 (oaner)Aley-nogunhtcoont elwecreertcon3859111892447827227ICPnogfinnNx1201NOMBKOF.DAO.ACAKUAN URBARA0FORxi7SO0M/WwothYo1K.CMhSF070.PKPCn0/7061Z0142a6097ANTOy01NOM8KOF.0AFUACBk6awFFThwBAR7BAOFFATEEAOFBAOFRAOFBAOURFGOBAOEBAYI.CEFPRV1OP2GiARhN0CP4DPtvZVBUTSoM-IIESAFOAYABom65/2FnAAnAFeu1-8633-482c-4614-aae8-S88da6aß92ehwwooryeconnectedsnulta<null)2825-4-18 14:88411829nk.9C.2810:2.2(provider user token encryptedIeyJod#[6T?NXeFR™bT2RbG1yZUE2cVpQQnE4aẞc9PSIsInZhbHVLẞoÏdktIMnZ2UGY3L8t423E1TnVJNkxDDU9XWTRTSnVDCmhBKz©2Uk5HThmYU9гbkM22HFvSWRNUL?MRG1·M2ttRU43SHRoRHJRVStvYßdKK3FnUFFWFhpV21GWnIvVHRVZz840kxXMG1YcBRSaHZnY304U*L2VVBDSTDprovider refresh token encryptedleyJod$t6InMcUHh6Z2F.74BxUF2TOHN3ZEtEWWC9PSIsInZhbHMLToiaUs5L2BXWW9GT2dcZBdhd102a105SMocN1B1eHFCaTK5TENnVnNPV2h0YnF0b1U3eWV=YmsrU0hnV2VKCEp2dCTST@1hYyZ6ImU3NTNSN5gcNnTxNWE1M2NkND142WNmMGUSY2B&YnYSOTBÄYTZmZGFmMmYy2TY321Mlencnyattion key (Hex)LBx040203907822.773677745265E0095SA75700E4ORERERO2REERE.CBAERZOSQ.ARLPJTR.7/.601282254521R20597BAB1232AA029534SE9099996EXA4A9/092A86/8065700010796/ raw rotriouod ctartina trom n h 626 me loydhtnh 86 ma totnhinn Onh ma1олл00124009UTG.8 Aenond...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87225
|
|
87224
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskreplayddeletedlanguage_server_macos_armscreenpipeWindowServermdsFirefoxCP Isolated Web Contenttrialdcoreaudiod3PSSlackFiretoxCP Isolated Web ContentbackupdsyspolicydWispr FlowFirefoxCP Isolated Web ContentcontainermanagerdiTerm2FirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr Flow Helper (Renderer)mds_stores220,1157,258,047,945,443,526,715,49,59,26,92,52,42,42,32,11,91,91,71,61,51,5HelpCPU Time4:18:58,7124:04:39,976:40:01,414,9111:58,223:49:59,388:06:35,1732:10,0840:59,825,401:01:48,0316:58,061:13,5517:28,104:22,9156,4725,781:04:59,9625:32,6743:53,425:42,191:10:31,17System:User:Idle:Threads26155932113666211335323$1281625302621CPUMemoryIdle Wake-Ups EnelKpHomeDMsActivityED→Jiminny ...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support100% <78• Thu 28 May 18:29:22QDescribe what you are looking for• lliyana Netseva• MessagesAdd canvas+една заявка (Asked hиилоример)Todayтука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsI.leae Vavalile c.ea neeN+37,79%62,21%0,00%CPUF8. Stefka Stoyanovao Stoyan TomovEo Petko Kashinski. Galya Dimitrova: Todor StamatovRo Steliyan GeorgievLo Ves% Mira. Nikolay Yankovdo James GrahamE Lukas Kovalik y...::: AppsJira CloudLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva+...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87224
|
|
87223
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
SlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUPhpStormkernel_taskreplayddeletedlanguage_server_macos_armscreenpipeWindowServermdsFirefoxCP Isolated Web ContenttrialdcoreaudiodbluetoothdmobileassetdActivity MonitortccdFirefoxopendirectorydFirefoxCP Isolated Web ContentbackupdsyspolicydWispr FlowFirefoxCP Isolated Web ContentcontainermanagerdiTerm2FirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentWispr Flow Helper (Renderer)mds_stores220,1157,258,047,945,443,526,715,49,59,26,93,63,23,23,12,92,62,52,42,42,32,11,91,91,71,61,51,5HelpCPU Time4:18:58,7124:04:39,976:40:01,414,9111:58,223:49:59,388:06:35,1732:10,0840:59,825,401:01:48,0321:02,8119,149:29,539:16,451:49:56,3511:13,2816:58,061:13,5517:28,104:22,9156,4725,781:04:59,9625:32,6743:53,425:42,191:10:31,17System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+43,97%51,51%4,52%CPUED→Jiminny ...# infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva4o Vasil Vasilev&. Stefka Stoyanovao Stoyan Tomovo Petko KashinskiGalya Dimitrova: Todor Stamatov. Steliyan GeorgievCo Ves% MiraP.. Nikolay Yankov4o James GrahamLukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:19Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87223
|
|
87222
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// 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
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// 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();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity tr...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87222
|
|
87221
|
Search
SELECT CONCAT(u.id, CASE WHEN u.id = t.owne Search
SELECT CONCAT(u.id, CASE WHEN u.id = t.owner_id TH...
SELECT * FROM team_features where team_id = 1; SEL...
Customize
Statements...
|
PhpStorm
|
|
NULL
|
87221
|
|
87220
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
7
149
1
33
SlackFileEditViewGoHistoryWindowHelpActivity MonitorAll ProcessesProcess Name% CPUCPU Timekernel_taskPhpStormscreenpipereplaydWindowServerdeletedmdsFirefoxCP Isolated Web Contentlanguage_server_macos_armtrialdlaunchservicesdcoreaudiodlaunchdcom.apple.geodbluetoothdFirefoxCP Isolated Web ContentmobileassetdClaudeFirefoxCP Isolated Web ContentActivity MonitorSlack Helper (Renderer)FirefoxCP Isolated Web Contentcom.apple.geodFirefoxCP Isolated Web ContentbackupdFirefoxiTerm2Slack191,724:04:31,63171,065,94:18:47,0361,43:49:57,076:39:58,3449,98:06:33,752,3732:09,2640:59,3111:55,814,911:04:21,921:01:47,6719:34,870,3721:02,622:38,6618,9729:38,6356,369:29,361:00:42,9843:53,330,3516:57,921:13,421:49:56,201:04:59,8614:04,28System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+46,05%40,90%13,05%CPUED→Jiminny...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana NetsevaEo Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan GeorgievCo Ves% MiraP.. Nikolay Yankov4o James GrahamLukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:17Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилоример)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87220
|
|
87219
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// 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
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// 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();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity tr...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87219
|
|
87218
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
7
149
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use HubSpot\Client\Crm\Owners\Model\PublicOwner;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// 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
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// 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();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity tr...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87218
|
|
87217
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
SlackFileEditViewGoHistoryWindowHelpActivity MonitorAll ProcessesProcess Name% CPUCPU Timekernel_taskPhpStormscreenpipereplaydWindowServerdeletedmdsFirefoxCP Isolated Web Contentlanguage_server_macos_armtrialdlaunchservicesdcoreaudiodlaunchdcom.apple.geodbluetoothdFirefoxCP Isolated Web ContentmobileassetdClaudeFirefoxCP Isolated Web ContentActivity MonitorSlack Helper (Renderer)FirefoxCP Isolated Web Contentcom.apple.geodFirefoxCP Isolated Web ContentbackupdFirefoxiTerm2Slack191,724:04:31,63171,065,94:18:47,0361,43:49:57,076:39:58,348:06:33,752,3732:09,2640:59,3111:55,814,911:04:21,921:01:47,6719:34,870,3721:02,622:38,6618,9729:38,6356,369:29,361:00:42,9843:53,330,3516:57,921:13,421:49:56,201:04:59,8614:04,28System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+49,52%36,50%13,98%CPUED→Jiminny...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana Netseva€o Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor Stamatov. Steliyan Georgievto Ves% MiraP.. Nikolay Yankov4o James GrahamLukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:14Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hимлоример)Today ~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е сьс статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87217
|
|
87216
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lProinet vCSamviceTesconrCMatchiet witermonts che=custom.loglaravel.lodA SF jiminny@localhostHSJocal [EMAIL]© RecordSelector.phg© Activity.phoA console (PRODesaastorce/Service.onmA console (EU) X uin users (EU# console [STAGINGM.INTERNALWEBHOOK SETU? maliminny storaceOpportunitySyncTrait.php© CrmEntityRepository.phgOo liminny vMalicenses.maclass service excenos baseservace olesentspublic function inportStages(?array Stypes = null, ?string $missingStageNane = null): ?StageA7 A149 V1 Y33 11 A Y 172select x fron text_relays where created at > *2026-85. 3031 49 A29 V 3 У 109 A 1Makere0 package-lock.jsonphpstan.neon.distF phpstan-baseline.neonphpunit.xmlraw.sol query.saMAesNeiMs ma448449450"tean_1dnane=> Sthis->tean->1d,wiath: 59)width:101)4S4'Label"'type=> ab_strinuidth(Ss['label')=> Stage::TYPE_OPPORTUNITY,seauence"=> $s["display0rder'),'is_selectable' => $s['active')proodozersy=> $s['probability") * 100start ei sonar-project propertiesEtest.py<> Untitied Diagram.xmlus vetur.config.isMAWISAHOeK SinEoiNGMOSUNif (SnissingStageNane === $s["id')) 4Duanostooetodehnt. Sytemallloranesv =0 Cemtches and Consolesv @ Database ConsolesV AEU# console cUA DEAL RISKS (EUustacesudostacrosSousiness?rocess-sstaces@-svnclsstages.r1724172717241І ПООИОЮ Т1Е ВЕ ВООНО О 10 ПЕ6НЕЕ К0ЮО 111 117291739select * fron activities order by id desclselect * fron users where name Like "Subrax":SELECT * FROM opportunities WHERE uwid to_bin(*@4a9cfad-2c87-4453-9e72-20aeb78ccf8d)gselect * Fron teams where 1d = 5555select * fron stages where tean id = 555CONCATCU,1d, CASE WHEN u,id = thouner id THEN * (ouner)* ELSE ** END) AS usenid!sa,*thonner id FROM sociall accounts seJoin users u on uid = sa,sochaoleoJOIN teans t 1.n<->1: on t.id = u.team_idAHERE u.tean_id = 100 and sa.provider = 'hubspot*:meutsunHIMI BEHISarvicaei7) Outputjiminny.opportunitiesIID fiminny stages(ID Result 11 xvontameAi W OrowsvSOL40IE6iBusen.fd 7menail yemidy : nsociable id n(m provider user id Tim provider user token1m provider refresh token : im expires ™i refresh token expires y:A console 1 $ 24 mgui users 1 $ 548 msy2liminny@localhostAus loasllA SFV A PROD# ConcalaV A STAGINGH Concal"noakorTO0У L7o nu comoy 1o:c8rleServiceTestCecsdales Orcnnworeeoeionhnves dohtine Oooond+0.RitoootaNorenhooer.60orobabuty" s Tloat (icatalcetadata"orobabto tym)hThis s the root causeHere's the full explanationRoot eause:moorStages overwmes D8 probabilty with tuospots valueThe flow is1. Someone manually changed stage 20616's probability in the Jiminny DB to 100.00 on 2826-45-44 . The stage crn provider 1d: "elosedlost" k2. ImportStages() runs (during any pipeline/stage sync), It callsservice.php:445-454Sstage = Sthis-contio-ostages-ithtrashed=updatcorcreateerorowideressio cetorosbliewr1+ CCooeS? Adaotiv# provider:(mstate: auth scope y : (mretry after vASVJ..O0updated atNMMaid Tosm.172227 WITSgensd...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87216
|
|
87215
|
IAlSlackFileEditViewGoHistoryWindowActivity Monito IAlSlackFileEditViewGoHistoryWindowActivity MonitorAll ProcessesProcess Name% CPUkernel_taskPhpStormreplaydPodcastsWindowServerscreenpipemdsamsengagementdFirefoxCP Isolated Web ContentFirefoxCP Isolated Web ContentfileproviderdcoreaudiodlaunchdlaunchservicesdbluetoothdActivity Monitoropendirectorydlogdlanguage_server_macos_armmds_storesNotion Calendar Helper (Renderer)FirefoxCP Isolated Web Contentclouddio.kandji.KandjiAgent.ESF-ExtensionFirefoxCP Isolated Web ContentfseventsdrunningboarddWispr Flow142,2129,968,425,419,116,311,711,38,88,87,35,65,43,43,43,32,82,72,72,52,42,32,22,12,01,91,81,8HelpCPU Time24:04:21,554:18:38,056:39:55,113,228:06:31,133:49:53,6032:08,650,9040:58,7343:53,151:22,511:01:47,3019:34,581:04:21,4621:02,399:29,1711:13,1317:10,4011:55,271:10:31,0210:03,9956,165,6832:03,5816:57.788:12,593:31,004:22,68System:User:Idle:Threads CPUMemoryEnelIdle Wake-UpsKp HomeDMsActivityFilesLater..•More+56,93%41,56%1,52%CPUED→Jiminny...#infrastructure_dev# jbu-team-info# jiminny-bg# platform-team# platform-tickets# product_launches# random# releases# support# thank-yous# the_people_of jimi...Direct messagesEL. Iliyana NetsevaEo Vasil Vasilev&. Stefka Stoyanovao Stoyan TomovEo Petko KashinskiGalya Dimitrova: Todor StamatovRo Steliyan GeorgievCo Ves% MiraP.. Nikolay Yankov4o James GrahamLukas Kovalik y...::: AppsJira Cloud(alo)100% <78• Thu 28 May 18:29:11Describe what you are looking forlliyana Netseva6 0• Messagest Add canvas+една заявка (Asked hиилопимер)Today~тука не съм сигурен, по-скоро е за продуктlliyana Netseva 4:00 PMok, thank youlliyana Netseva 5:45 PMИмам питане и се чудя кьм кого мога да сеобърна?става дума за deal insightsLukas Kovalik 5:54 PMПитай, ако мога ще отговоряlliyana Netseva 5:56 PMимам сделка която е със статус won но е вclosed losttova e ID-to 04a9cfad-2c87-4453-9e72-20aeb78ccf8d na sdelkataLukas Kovalik 6:00 PMUS или EU (edited)lliyana Netseva 6:01 PMeuLukas Kovalik 6:27 PMвиждам че си отворила тикет, ще го видя утренещо се умазало sync на stagesмисля клиент е променил нещо трябва да говидя в самия hubspotMessage Iliyana Netseva...
|
PhpStorm
|
faVsco.js – console [EU]
|
NULL
|
87215
|