|
9165
|
411
|
6
|
2026-05-08T12:13:34.958257+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242414958_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9163
|
NULL
|
NULL
|
NULL
|
|
9164
|
412
|
11
|
2026-05-08T12:13:34.388161+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242414388_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.38430852,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39394948,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4012633,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9162
|
NULL
|
NULL
|
NULL
|
|
9163
|
411
|
5
|
2026-05-08T12:13:01.596250+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242381596_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5545028788199783425
|
-8204141067936470074
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
SlackFileEditViewGoHistoryWindowHelp> 0 lbl• Support Daily • 2 m leftAPP (-zsh)DOCKERDEV (docker)882APP (-zsh)883-zsh• 84PHPruntime:8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php"5663/5663100%screenpipe"Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pullremote: Enumerating objects: 15,done.remote: Counting objects: 100% (15/15), done.remote: Compressing objects: 100% (2/2), done.remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.From github.com:jiminny/appc57e71e763..8743fea32e* [new branch]Already up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $JY-20606-desktop-app-recall-> origin/JY-20606-desktop-app-recallJY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit100% C8Fri 8 May 15:13:02•$5-zsh₴6APP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9162
|
412
|
10
|
2026-05-08T12:13:00.425828+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242380425_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
PhostormVIew Project: faVsco.js, menu
master, menu
PhostormVIewINavigareCodeLaravelKeractorFV faVsco.js?9 masterProiect•JiminnyDebugcommand.ongm ServiceTraitsn. DeleteCrmEntityraic.ongo kematcnactiviyoncrmobjectbetach.org© CheckAndRetryRemoteMatch.php(C) MatchActivityCrmData.ong© DataClient.php© DecorateActivity.php(c) LocalSearch.onp© CrmObjectsResolver.phpo kemotesearen.ong© Service.phpv @ Listeners© ConvertLeadActivitieC Purgelookupcacne..0 Metadata>- Migrationu Pipedriveu salestorceW Fields> OpportunitvMatchenOpportunitvSvncStra> ProspectSearchStratM Service Traitsc) clientoho© DecorateActivitv.ohr() DeleteObiectsTrait.ol 22:C) FieldDefinitions.onv230© PayloadBuilder.php© Profile.php© QueryBuilder.php@ QuervHandler.php© Querylterator.php@ QuervResults.php© Service.php© SyncBatchRedisServTm Traite232© BaseClient.php© BaseService.php© CachedCrmServiceDecc 249© CountryCodeResolver.pl 257CrmActivitvProviderinte© CrmActivityService.php© CrmConfigurationSettin© CrmObiectsResolver.phi 260© DefaultProspectSearchs262c) Email-eloer.oho@ FindsProspectinterface, 281C) LavoutManager.oho(1) MatchDomain3vEmailint@ OpnortunitvActivitvMatc(1) OnportunitvSvncStratea(c) Procnectcache nhn8 ProsnentSearchSconen© ProspectSearchStrategy 324class Prospectcachepublic function fand prospectidentifierreturn Sresult.public function findDomainMatch(Configuration Sconfiguration, string $identifier, ?int SuserId = null): ?arral?public function set(Configuration Sconfiguration, string $identifier, array $prospectData, ?int SuserId = nuz13public function handleProspectUpdated(ProspectUpdated Sevent): void{...}public function normalizePhoneNumber(string $phone): string{...}frivate funetion gettconfaguration Sconfiguration, Sträng Sidentifien, Pänt SuseniG E nUlL): Forrareturn $this->cache->tags($this->getTags($configuration, $identifier))-›aet Sthis->generatekeyconfiquration.Sidentifier. Suserid)):private function generateKev(Configuration Sconfiquration, string Sidentifier. ?int SuserId = null): string{.27onivate function sendDatadoaStats(strina Sresult. string Scrm)• void!...?1 usageprivate function getCacheTtl: intf...}nnivate function findûnnontunitvTnContactRolec(Confiaunation Sconfiaunationl?Profile Sprofileint Scontac+td): ?Opportunity {...}private function getOpportunityFromDatabase(contquraron scontzqurarzon.Aosount Saccount.int Scontactid.Pint SuserId = nuli): 20pportunity {...}suppont Dally • 2m len100% 12Fri 8 May 15:13:01AskJiminnyReportActivityServiceTest v+0 ..4 SF jiminny@localhost]A HS_local [jiminny@localhost]# console [PKol)A console [EU]A console [STAGING][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"neaders".?"Uace"."Inu,or May 2020 14.21.15 6Ml"Jn"concenc-lyoe. apolicacionison.charser=uct-o.n"Transter-Encodinq":"chunked")n"CF-Ray":"9t80deb8dbo0dcsa-S0F""Strict-Transport-Secur1ty":"max-aqe=31536008* 1ncLudeSubDomains: preload"),M X19 A V"server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\","x-content-type-options": ["nosniff"],"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"Set-Cookie":["__cf_bm=StUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxT07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Renont-To".f"y"endnoints".\"url\":\"https:|\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgl\"group\":\"cf-nel\".\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,reportco. "cr-nel,"max age| ":604800}"]."Server": ["cloudflare"]}} {"correlation_1d": "95256555-ec98-4541-D92a-adta/Sboyead","trace_10":C/AD8565-905t-4604-9405-0e5b551e55455CascadeHubspot Rate LimitingHubSpot Rate Limit ReInvestigating Rate LinThere is no cache laver at the decorator level Tor name matching — it delegates straight to the service4. getPaginatedData fetches ALL pagesmatchByName calls getPaginatedData() (not the generator) which eagerly fetches all pages of contact results. generateSearchContactsByNamePayload uses CONTAINS TOKEN which can match many contacts — every page is a separate APl request.5. usleep($delay) is per-job, not per-team• HubspotPaginationService.php:39poelay = schis-catcularevelay.nmacrosecongs.g• PaginationConfig.php:10public const int SEARCH_RPS LIMIT = 4;Ine delay is 1/4 * 1,000,000 = 250ms per request per job instance • With N Jobs running in parallel, the enfective rate is 4 * N Rps TromHubSpot's perspective. HubSnot's search endooint limit is 5 RPS alobally per portal _ with iust 2 concurrent iobs the limit is already blownSummarv of Root CausesImpactNo rate limitina coordination across iobsMultiole workers hit HubSoot simultaneouslv. multiolvina RPSmatchßvName has no decorator-level cacheSame name searched repeatedly by different jobsmatchßvName cache TiLis onlv 60s, scoved to userldCache busts trequentiv. same contact name searched N timecgetPaginatedData fetches all pages eagerlyoportunities chained APl calOne participant match = 2+ paginated searchesN activities dispatched = N independent concurrent iobsSuggested ImprovementsImmediate " Hiah moact1.Add decorator-level cachina.for.matchBvvame.in.cachedcimServicedecorator-scope.the cache.kev toconfiald_+namenot usenid withalonger iL.e.g.omin. contact names con u change per-user2. Use the generator instead of getPaginatedData in matchBvName — stoo after first result with →current() , don't fetch all pages.3. Implement a team-scoped Redis rate limiter (e.g. sliding window) in HubspotPaginationService:: executeSearchRequest() to enforce ≤5RPS across all workers for the same portalA Catch GuzzleHttnl Eycention\ClientFycention with 120 and imnlement eynonential backoff — currentlv the eycention oronadates unhandledand the iob retries after (30. 98. 180] seconds, but the root cause (other jobs) is still running.5. Increase matchByName cache TTL and remove userId from cache key — a contact named "John Smith" is the same regardless of which repsearched for them.wif we would place matchByName to CachedCrmServiceDecorator same as @CachedCrmServiceDecorator.php#L56-74 it would still cache byC° AdantiveWN Windsurf Teams 222•6 UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9161
|
412
|
9
|
2026-05-08T12:12:47.306988+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242367306_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.38430852,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39394948,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4012633,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9160
|
NULL
|
NULL
|
NULL
|
|
9160
|
412
|
8
|
2026-05-08T12:12:43.074495+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242363074_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4964913726732674038
|
-3048922505555752502
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9159
|
412
|
7
|
2026-05-08T12:12:37.873782+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242357873_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.38430852,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39394948,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4012633,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9157
|
NULL
|
NULL
|
NULL
|
|
9158
|
411
|
4
|
2026-05-08T12:12:37.873775+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242357873_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9156
|
NULL
|
NULL
|
NULL
|
|
9131
|
409
|
10
|
2026-05-08T12:09:21.567541+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242161567_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9128
|
NULL
|
NULL
|
NULL
|
|
9130
|
410
|
11
|
2026-05-08T12:09:07.957719+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242147957_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.38430852,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39394948,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4012633,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9127
|
NULL
|
NULL
|
NULL
|
|
9129
|
410
|
10
|
2026-05-08T12:08:37.153390+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242117153_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.38430852,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39394948,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4012633,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9127
|
NULL
|
NULL
|
NULL
|
|
9128
|
409
|
9
|
2026-05-08T12:08:35.324476+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242115324_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9127
|
410
|
9
|
2026-05-08T12:08:05.981595+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242085981_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.38430852,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39394948,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4012633,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9126
|
409
|
8
|
2026-05-08T12:08:04.534238+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242084534_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9125
|
NULL
|
NULL
|
NULL
|
|
9125
|
409
|
7
|
2026-05-08T12:07:30.249967+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242050249_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8243381250999052583
|
-8204424741936591934
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
SlackFileEditViewGoHistoryWindowHelpAPP (-zsh)>0 lbl.• 84Support Daily - now100% C8Fri 8 May 15:07:33DOCKERDEV (docker)882APP (-zsh)883-zshPHPruntime:8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from"-php-cs-fixer.dist.php"5663/5663100%Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pullremote: Enumerating objects: 15,remote: Counting objects: 100% (15/15), done.remote: Compressing objects: 100% (2/2), done.remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.From github.com:jiminny/appc57e71e763..8743fea32e* [new branch]Already up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $JY-20606-desktop-app-recall-> origin/JY-20606-desktop-app-recallJY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limitSupport Dailynow - 15:00-15:15• Support Daily - 2026/05/... +1 moreC Join Google MeetAPP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9124
|
409
|
6
|
2026-05-08T12:07:27.805458+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242047805_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9121
|
NULL
|
NULL
|
NULL
|
|
9123
|
410
|
8
|
2026-05-08T12:07:27.326626+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778242047326_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6615692,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.67287236,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.68018615,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.4275266,"top":0.09736632,"width":0.5724734,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.4275266,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.4275266,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.4275266,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.4275266,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.4275266,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.4275266,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.4275266,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.4275266,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.4275266,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.4275266,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.4275266,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.4275266,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.4275266,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.4275266,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.4275266,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.4275266,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.4275266,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.4275266,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.4275266,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.4275266,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.4275266,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.4275266,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.4275266,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.4275266,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.38430852,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39394948,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4012633,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
920666202760937713
|
338922648065674223
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
9122
|
NULL
|
NULL
|
NULL
|
|
38526
|
1427
|
7
|
2026-05-13T17:25:10.923224+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-13/1778 /Users/lukas/.screenpipe/data/data/2026-05-13/1778693110923_m1.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Postman File Project: faVsco.js, menu
master, menu
Postman File EditViewWindowHelpA100% <8• Wed 13 May 20:25:10DOCKERO 81DEV (docker)₴2APP (-zsh)83ec2-user@ip-10-20-31-146:~-zsh|84screenpipe"О 85ec2-user@ip-10-30-129-...886ec2-user@ip-10-20-31-14... #7[2026-05-13 15:28:23Jproduction.INFO:[SocialAccountService] Fetching"trace_id":"1a72fef6-427a-4e63-a9ba-b340103c976b"}token {"socialAccountId":30110, "provider": "hubspot"} {"correlation_id":"edla364d-0b07-4c47-95a7-d22fd8ef6bec[2026-05-13 15:28:23] production.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":30110, "provider": "hubspot"} {"correlation_id": "ed1a364d-0b07-4c47-95a7-d22fd8ef6bec","trace_id":"1a72fef6-427a-4e63-a9ba-b340103c976b"}[2026-05-13 15:28:23] production.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"ed1a364d-0b07-4c47-95a7-d22fd8ef6bec"fef6-427a-4e63-a9ba-b340103c976b"},"trace_id":"1a72[2026-05-13 15:28:23] production.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":30110, "provider": "hubspot", "refreshToken" : "9417a6a067cd68efa0bd023e970cc27482ef7db27b876a4383f5a246c4e8d81c", "state": "full-refresh"} {"correlation_id":"edla364d-0b07-4c47-95a7-d22fd8ef6bec", "trace_id": "1a72fef6-427a-4e63-a9ba-b340103c976b"}[2026-05-1315:28:23Jproduction.ERROR: [SocialAccountService] Failedto refresh token {"socialAccountId" :30110, "provider" : "hubspot",age\":\"missing or unknown hub id\","responseBody":"{\"status\":\"BAD_HUB\", \"mess,\"correlationId\":\"019e21f4-1184-72ca-8C79-d9e09814baa4\", \"error)":\"access_denied\", \"error_description)":\"missing or unknown hub idl"}"3 {"correlation_id":"ed1a364d-0b07-4c47-95a7-d22fd8ef6bec", "trace_id":"1a72fef6-427a-4e63-a9ba-b340103c976b"}[2026-05-13 15:28:23] production.INF0: [SocialAccountObserver] Saving model {"correlation_id":"ed1a364d-0b07-4c47-95a7-d22fd8ef6bec","trace_id" :"1a72fef6-427a-4e63-a9ba-b340103c976b"}Flow refresh required.root@453da0675541:/home/jiminny# php artisan jiminny:token-info -A 30110-R[2026-05-13 15:28:31] production.INF0: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command": "jiminny:token-info"."memoryBeforeCommandInMb" : 116.0,"memoryPeakBeforeCommandInMb":116.0} {"correlation_id":"ble2505a-8c60-4607-a96a-a33209efd4c4", "trace_id":"001e9c48-df0f-4111-9988-d3bb8bf7bfa8"}[2026-05-13 15:28:31] production.INFO: [SocialAccountService] Fetching token {"socialAccountId":30110, "provider": "hubspot"} {"correlation_id":"b1e2505a-8c60-4607-a96a-a33209efd4c4, "trace_id": "001e9c48-df0f-4111-9988-d3bb8bf7bfa8"}[2026-05-13 15:28:31] production.INF0: [SocialAccountService]Token needs refreshing {"socialAccountId":30110,"provider": "hubspot"} {"correlation_id":"ble2505a-8c60-4607-a96a-a33209efd4c4", "trace_id":"001e9c48-df0f-4111-9988-d3bb8bf7bfa8"}[2026-05-13 15:28:31] production.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"ble2505a-8c60-4607-a96a-a33209efd4c4"9c48-df0f-4111-9988-d3bb8bf7bfa8"}"trace_id":"001e[2026-05-13 15:28:31] production.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":30110, "provider": "hubspot","refreshToken": "9417aбa067cd68efa0bd023e970cc27482ef7db27b876a4383f5a246c4e8d81c","state": "full-refresh"} {"correlation_id":"ble2505a-8c60-4607-a96a-a33209efd4c4".',"trace_id":"001e9c48-df0f-4111-9988-d3bb8bf7bfa8"}[2026-05-13 15:28:32] production.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":30110, "provider": "hubspot","responseBody":"{\"status\":\"BAD_HUB\", \"message\":\"missing or unknown hub id\",\"correlationId\":\"019e21f4-319c-7501-8b0e-d3118c6534f8\".,\"error)":\"access_denied\", \"error_description)":\"missing or unknown hub id\"}"}"correlation_id": "b1e2505a-8c60-4607-a96a-a33209efd4c4", "trace_id": "001e9c48-df0f-4111-9988-d3bb8bf7bfa8"}[2026-05-13 15:28:32] production.INFO: [SocialAccountObserver] Saving model {"correlation_id":"ble2505a-8c60-4607-a96a-a33209efd4c4", "trace_id": "001e9c48-df0f-4111-9988-d3bb8bf7bfa8'"}Flow refresh required.root@453da0675541:/home/jiminny# [ec2-user@ip-10-20-31-146 ~]$ 0...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
38525
|
1428
|
10
|
2026-05-13T17:25:09.664036+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-13/1778 /Users/lukas/.screenpipe/data/data/2026-05-13/1778693109664_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormProleteycodeFV faVsco.js© TextRelayService PhostormProleteycodeFV faVsco.js© TextRelayService.php= custom.logscratch &.isonA SF jiminny@localhost]& console [PROD]• m ServiceTraitsA console (EU]tid stages [EU]fiò teams (EU]© Activity.php X A console [STAGING]© DataClient.phpclass Activity extends Model implementsODecordteacuvily.ongc) LocalSearch.ongC Textkelay.pnpnd.php© RemoteSearch.php© Service.phpC) UpdateActivityElasticSearchDocumentCommand.php© ConferenceCrmMatcherJob.php© ImportParticipants.phpv D ListenersC) UpdateSinaleEntity.oho©) ActivityStatusin.phpc) convertLead ActivilieC) ProspectCache.php X41 ^ y 2089c) Purgelookupcacne..> C) Metadata> Migrationa Pipedrivev u salestorce> Fields• OpportunityMatcherclass ProspectCacheprivate function getOpportunityFromDatabase285Accounc saccounc.int ScontactId?int SuserId = nullUpportunicy"Sopportunity = null:• OpportunitySvncStraif (SuserId) {> ProspectSearchStrat• h Service fraitsc) clientoho@ DecorateActivitv.ohe• Searching DB for opportunity by owner', [= saccount->qetido."contact 1d => scontactiid.DeleteObiectsTrait.oowner 1d => SuseriidiC) FieldDefinitions.onv© PayloadBuilder.php€ Profile.php© QueryBuilder.php© QueryHandler.php© Querylterator.php© QueryResults.php(c) Service.php© syncBatchRedisServTroite(c) BaseClient.php© BaseService.php1):Sonnortunitv = Sthis->onnortunitvRenositorv->find0neßvAccountAnd0oportunztv0wnerdSconflauration.SaccountSuserId,Scontac+Tdlif (! Sopportunity instanceof Opportunity) {Sthis->logger->info('ProspectCache• Fallback DB opportunity search', ['account_ id' => $account->getId."contact1d = scontaccla,c) countrvcodeResolver.pCrmActivityProviderinteSopportunity = Sthis->opportunityRepository->find0neByAccountAndOpportunityAssiqnmentRule(c) crmcontiqurationSettinc) crmobiectsResolver.phc) DeraultProsoectSearchsc) Email-eloer.oho(1) FindsProspectinterface316ppportunity DB search results', l'account_id' => $account->getid A(1) MatchDomain3vEmailin1(C) OpportunitvActivitvMatc'opportunity id' => Sooportunitv?->getidor(1) OnportunitvSvncStratec(c) OnnortunitvSvneStratec(c) Procnectcache nhn8 ProcnectSearchScone n(C) ProcnectSearchStratear2091211821192126LeadinulzAccount|null,Opportunity|null,Contact|null,scagelnull,scringinuce*} Srecordspubuic tunccion updaceAccivicyurmuacalarray srecoras: vo1a// Extract the recordsslead, saccount, sopporcunity, sconcact, sscage = srecoras:Sresolver = schis-›qetupdareurmuarakesouverosstrareoy = sresolver-›reso veroractvirv slead, scontact, saccount)sif ($strategy == UpdateCrmDataByStrategy::Lead) {II Also update the parent activity if required. checking we don't create a mixed lead/account record.if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {Sthis->lead 1d = slead->1d5if (Sthis->stage id === null &s Sstage) {Sthis->stage id = Sstage->id.} elseif ($strategy == UpdateCrmDataByStrategy::Contact)// Also update the parent activity if required, checking we don't create a mixed lead/account record.Sthic-slead id = null.if (Sthis->stage && $this->stage->getType() === Stage::TYPE_LEAD) {Sthic-sctade id = null.// Don't trust previous matched account id as it might have been changed in the CRMif (Saccount && Saccount->id !== Sthis->account id) {Sthis->account id = Saccount->idif (Sthis->stage id === null && Sstage) {Sthis->stage_1d = Sstage->1d:if (Sopportunitv && $this->onportunitv id 1== Sonnortunitv->id) {Sthis->ooportunitv id = Soonortunitv->idiif (Sonpontunity &s Sthis->value I== Sonnortunitv->value)&Sthic->value = Connontunitv->valup:hhl100% L2• Wed 13 May 20:25:09AskJiminnyReportActivityServiceTest vcascadeTrial Owner Role SeleRetactoring User EmaActivity Stage ID Upda+0..Purpose: "validate all final participants, and run crm matchina for them. in case an opportunity was created durina theFlow1. ConferenceCrmMatcherJob (Line 41-43)• Logs: "Trying to refresh activity crm data"Calls ImoortParticioants->refreshCrmData0l2. ImportParticipants->refreshCrmData() (Line 177-193)• Loons throuch particinants•Calls participantCrmLookupo for each3. particioantCrmLookup( Line 283-332• Loas: "Prospect attendee found. Attemotina to match to a CRM recordi• Calls findProspectDataByEmail() (Line 296)4. tindProspectDataByEmallo (Line 399-433)•Uses CachedCrmServiceDecorator to search• Loas show: "Searchina HK for opportunity by owner" (cache search)• Logs show: "Opportunity DB search results" (found in cache)• Returns cached CRM records5 Cache Hit (Loac chow)•"Prospect match: Cache / local search hiti•Returns existing opportunity data: { opportunity: 7842553, stage: 18775 }•Gets original activity data: & stage: 18775 }• Calls Activity->updateActivityCrmData() (Line 121)•Gets current activity data: stage: 18775• Loas: "Undated activity crm data" showina oriainal data == current dataKey FindingThe stage id was Not changed by this 10bThe loa shows#oriainal data". { "ctaae". 18775 }• stage id: 18775 was already set on the activity before ConferenceCrmMatcher Job ranlThe ioh confirmed the eyictina data via cache hit• No actual chanae occurrediWhen Was stace id Initially Set?Since this job at 08:21:40 didn't change the stage id (it was already 18775), and your activity was created at 08:00:00Ask anvthina (84L)« Code SWF-1.6WN Windsurf Toams 216-46UTF.8io 4 spaces...
|
NULL
|
8216859535635261493
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormProleteycodeFV faVsco.js© TextRelayService PhostormProleteycodeFV faVsco.js© TextRelayService.php= custom.logscratch &.isonA SF jiminny@localhost]& console [PROD]• m ServiceTraitsA console (EU]tid stages [EU]fiò teams (EU]© Activity.php X A console [STAGING]© DataClient.phpclass Activity extends Model implementsODecordteacuvily.ongc) LocalSearch.ongC Textkelay.pnpnd.php© RemoteSearch.php© Service.phpC) UpdateActivityElasticSearchDocumentCommand.php© ConferenceCrmMatcherJob.php© ImportParticipants.phpv D ListenersC) UpdateSinaleEntity.oho©) ActivityStatusin.phpc) convertLead ActivilieC) ProspectCache.php X41 ^ y 2089c) Purgelookupcacne..> C) Metadata> Migrationa Pipedrivev u salestorce> Fields• OpportunityMatcherclass ProspectCacheprivate function getOpportunityFromDatabase285Accounc saccounc.int ScontactId?int SuserId = nullUpportunicy"Sopportunity = null:• OpportunitySvncStraif (SuserId) {> ProspectSearchStrat• h Service fraitsc) clientoho@ DecorateActivitv.ohe• Searching DB for opportunity by owner', [= saccount->qetido."contact 1d => scontactiid.DeleteObiectsTrait.oowner 1d => SuseriidiC) FieldDefinitions.onv© PayloadBuilder.php€ Profile.php© QueryBuilder.php© QueryHandler.php© Querylterator.php© QueryResults.php(c) Service.php© syncBatchRedisServTroite(c) BaseClient.php© BaseService.php1):Sonnortunitv = Sthis->onnortunitvRenositorv->find0neßvAccountAnd0oportunztv0wnerdSconflauration.SaccountSuserId,Scontac+Tdlif (! Sopportunity instanceof Opportunity) {Sthis->logger->info('ProspectCache• Fallback DB opportunity search', ['account_ id' => $account->getId."contact1d = scontaccla,c) countrvcodeResolver.pCrmActivityProviderinteSopportunity = Sthis->opportunityRepository->find0neByAccountAndOpportunityAssiqnmentRule(c) crmcontiqurationSettinc) crmobiectsResolver.phc) DeraultProsoectSearchsc) Email-eloer.oho(1) FindsProspectinterface316ppportunity DB search results', l'account_id' => $account->getid A(1) MatchDomain3vEmailin1(C) OpportunitvActivitvMatc'opportunity id' => Sooportunitv?->getidor(1) OnportunitvSvncStratec(c) OnnortunitvSvneStratec(c) Procnectcache nhn8 ProcnectSearchScone n(C) ProcnectSearchStratear2091211821192126LeadinulzAccount|null,Opportunity|null,Contact|null,scagelnull,scringinuce*} Srecordspubuic tunccion updaceAccivicyurmuacalarray srecoras: vo1a// Extract the recordsslead, saccount, sopporcunity, sconcact, sscage = srecoras:Sresolver = schis-›qetupdareurmuarakesouverosstrareoy = sresolver-›reso veroractvirv slead, scontact, saccount)sif ($strategy == UpdateCrmDataByStrategy::Lead) {II Also update the parent activity if required. checking we don't create a mixed lead/account record.if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {Sthis->lead 1d = slead->1d5if (Sthis->stage id === null &s Sstage) {Sthis->stage id = Sstage->id.} elseif ($strategy == UpdateCrmDataByStrategy::Contact)// Also update the parent activity if required, checking we don't create a mixed lead/account record.Sthic-slead id = null.if (Sthis->stage && $this->stage->getType() === Stage::TYPE_LEAD) {Sthic-sctade id = null.// Don't trust previous matched account id as it might have been changed in the CRMif (Saccount && Saccount->id !== Sthis->account id) {Sthis->account id = Saccount->idif (Sthis->stage id === null && Sstage) {Sthis->stage_1d = Sstage->1d:if (Sopportunitv && $this->onportunitv id 1== Sonnortunitv->id) {Sthis->ooportunitv id = Soonortunitv->idiif (Sonpontunity &s Sthis->value I== Sonnortunitv->value)&Sthic->value = Connontunitv->valup:hhl100% L2• Wed 13 May 20:25:09AskJiminnyReportActivityServiceTest vcascadeTrial Owner Role SeleRetactoring User EmaActivity Stage ID Upda+0..Purpose: "validate all final participants, and run crm matchina for them. in case an opportunity was created durina theFlow1. ConferenceCrmMatcherJob (Line 41-43)• Logs: "Trying to refresh activity crm data"Calls ImoortParticioants->refreshCrmData0l2. ImportParticipants->refreshCrmData() (Line 177-193)• Loons throuch particinants•Calls participantCrmLookupo for each3. particioantCrmLookup( Line 283-332• Loas: "Prospect attendee found. Attemotina to match to a CRM recordi• Calls findProspectDataByEmail() (Line 296)4. tindProspectDataByEmallo (Line 399-433)•Uses CachedCrmServiceDecorator to search• Loas show: "Searchina HK for opportunity by owner" (cache search)• Logs show: "Opportunity DB search results" (found in cache)• Returns cached CRM records5 Cache Hit (Loac chow)•"Prospect match: Cache / local search hiti•Returns existing opportunity data: { opportunity: 7842553, stage: 18775 }•Gets original activity data: & stage: 18775 }• Calls Activity->updateActivityCrmData() (Line 121)•Gets current activity data: stage: 18775• Loas: "Undated activity crm data" showina oriainal data == current dataKey FindingThe stage id was Not changed by this 10bThe loa shows#oriainal data". { "ctaae". 18775 }• stage id: 18775 was already set on the activity before ConferenceCrmMatcher Job ranlThe ioh confirmed the eyictina data via cache hit• No actual chanae occurrediWhen Was stace id Initially Set?Since this job at 08:21:40 didn't change the stage id (it was already 18775), and your activity was created at 08:00:00Ask anvthina (84L)« Code SWF-1.6WN Windsurf Toams 216-46UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
38523
|
1428
|
8
|
2026-05-13T17:24:57.496817+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-13/1778 /Users/lukas/.screenpipe/data/data/2026-05-13/1778693097496_m2.jpg...
|
PhpStorm
|
faVsco.js – ProspectCache.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
Analyzing…
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: ...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"bounds":{"left":0.37200797,"top":0.22426178,"width":0.019946808,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Illuminate\\Contracts\\Cache\\Repository;\nuse Illuminate\\Support\\Facades\\App;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Events\\Crm\\ProspectUpdated;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\Crm\\CrmObjects\\Validators\\StaleRecordValidator;\nuse Jiminny\\Services\\Crm\\Salesforce\\OpportunityMatcher\\MatchBusinessAccount;\nuse Psr\\Log\\LoggerInterface;\n\nclass ProspectCache\n{\n public const string PROSPECT_TYPE_EMAIL = 'email';\n public const string PROSPECT_TYPE_PHONE = 'phone';\n\n public const string PROSPECT_TYPE_DOMAIN = 'domain';\n private const int TTL_SECONDS = 900;\n private const int TTL_SECONDS_DEV = 30;\n private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';\n private const string LOOKUP_RESULT_INTERNAL = 'internal';\n private const string LOOKUP_RESULT_CACHE = 'cache';\n private const string LOOKUP_RESULT_DB = 'db';\n private const string LOOKUP_RESULT_MISS = 'miss';\n\n public function __construct(\n private readonly FindsProspectInterface $dbCache,\n private readonly Repository $cache,\n private readonly OpportunityRepository $opportunityRepository,\n private readonly EmailHelper $emailHelper,\n private readonly LoggerInterface $logger,\n private readonly StaleRecordValidator $staleRecordValidator,\n ) {\n }\n\n /**\n * @return null|array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * }\n */\n public function findByProspectIdentifier(\n Configuration $configuration,\n ?Profile $profile,\n string $identifierType,\n string $identifierValue,\n ?int $userId = null,\n ?SyncCrmEntitiesInterface $crmService = null\n ): ?array {\n $cachedValue = $this->get($configuration, $identifierValue, $userId);\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());\n\n return $cachedValue;\n }\n\n if ($identifierType == self::PROSPECT_TYPE_EMAIL\n && $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)\n ) {\n // Set the cache to avoid querying the database for internal participants\n $prospectData = [null, null, null, null, null, null];\n $this->set($configuration, $identifierValue, $prospectData, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());\n\n return $prospectData;\n }\n\n $dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);\n\n if (empty(array_filter($dbCache))) {\n $this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());\n\n return null;\n }\n\n $dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);\n $dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);\n\n if ($dbCache['contact'] instanceof Contact) {\n $account = $dbCache['contact']->getAccount();\n $dbCache['account'] = $account;\n\n $opportunity = $this->findOpportunityInContactRoles(\n $configuration,\n $profile,\n $dbCache['contact']->getId()\n );\n\n $opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);\n\n if ($opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Found opportunities by contact roles', [\n 'opportunity_id' => $opportunity->getId(),\n ]);\n $dbCache['account'] = $opportunity->getAccount();\n } elseif ($account instanceof Account) {\n $opportunity = $this->getOpportunityFromDatabase(\n configuration: $configuration,\n account: $account,\n contactId: $dbCache['contact']->getId(),\n userId: $userId\n );\n }\n\n $dbCache['opportunity'] = $opportunity;\n $dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);\n\n $dbCache['stage'] = $dbCache['opportunity']?->getStage();\n }\n\n /**\n * @IMPORTANT The keys must always be in this exact order\n *\n * @var array{\n * ?Lead,\n * ?Account,\n * ?Opportunity,\n * ?Contact,\n * ?Stage,\n * string|null\n * } $result\n */\n $result = [\n $dbCache['lead'] ?? null,\n $dbCache['account'] ?? null,\n $dbCache['opportunity'] ?? null,\n $dbCache['contact'] ?? null,\n $dbCache['stage'] ?? null,\n null,\n ];\n\n $this->set($configuration, $identifierValue, $result, $userId);\n $this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());\n\n return $result;\n }\n\n public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n $cachedValue = $this->get(\n configuration: $configuration,\n identifier: $identifier,\n userId: $userId\n );\n\n if ($cachedValue !== null) {\n $this->sendDatadogStats(\n self::LOOKUP_RESULT_CACHE,\n $configuration->getProviderName()\n );\n\n return $cachedValue;\n }\n\n // Log cache miss\n $this->logger->info('[Prospect match] Cache miss', [\n 'identifier_type' => self::PROSPECT_TYPE_DOMAIN,\n 'identifier' => $identifier,\n 'crm' => $configuration->getProviderName(),\n ]);\n\n // not in the cache\n return null;\n }\n\n public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void\n {\n $this->cache->tags($this->getTags($configuration, $identifier))->put(\n $this->generateKey($configuration, $identifier, $userId),\n $prospectData,\n $this->getCacheTtl()\n );\n }\n\n public function handleProspectUpdated(ProspectUpdated $event): void\n {\n $prospect = $event->getProspect();\n $configuration = $prospect->getCrmConfiguration();\n\n if ($configuration === null) {\n return;\n }\n\n if ($prospect->getEmail() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();\n }\n\n if ($prospect->getPhone() !== null) {\n $normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());\n $this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();\n }\n\n if ($prospect->getName() !== null) {\n $this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();\n }\n }\n\n public function normalizePhoneNumber(string $phone): string\n {\n // Remove all non-digit characters first\n $digitsOnly = preg_replace('/[^\\d]/', '', $phone);\n\n // Remove a single leading zero if present\n $digitsOnly = ltrim($digitsOnly, '0');\n\n // Add E.164 prefix\n return '+' . $digitsOnly;\n }\n\n private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array\n {\n return $this->cache->tags($this->getTags($configuration, $identifier))\n ->get($this->generateKey($configuration, $identifier, $userId));\n }\n\n private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string\n {\n $keySuffix = $userId === null ? '' : ':user:' . $userId;\n\n return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);\n }\n\n private function sendDatadogStats(string $result, string $crm): void\n {\n Datadog::increment(self::DATADOG_STAT_NAME, 1, [\n 'result' => $result,\n 'crm' => $crm,\n ]);\n }\n\n private function getCacheTtl(): int\n {\n if (! App::environment('production', 'production-eu')) {\n return self::TTL_SECONDS_DEV;\n }\n\n return self::TTL_SECONDS;\n }\n\n private function findOpportunityInContactRoles(\n Configuration $configuration,\n ?Profile $profile,\n int $contactId\n ): ?Opportunity {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $contactRoles = $contactRoleRepository->getByCrmContactId(\n $configuration,\n $contactId,\n );\n\n if (! $contactRoles->isEmpty()) {\n $opportunityId = app(MatchBusinessAccount::class)->resolve(\n $contactRoles,\n $profile?->getCrmProviderId(),\n );\n\n $opportunity = $this->opportunityRepository->find($opportunityId);\n }\n\n return $opportunity ?? null;\n }\n\n private function getOpportunityFromDatabase(\n Configuration $configuration,\n Account $account,\n int $contactId,\n ?int $userId = null\n ): ?Opportunity {\n $opportunity = null;\n\n if ($userId) {\n $this->logger->info('ProspectCache - Searching DB for opportunity by owner', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'owner_id' => $userId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(\n $configuration,\n $account,\n $userId,\n $contactId\n );\n }\n\n if (! $opportunity instanceof Opportunity) {\n $this->logger->info('ProspectCache - Fallback DB opportunity search', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n ]);\n $opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(\n $configuration,\n $account,\n $contactId\n );\n }\n\n $this->logger->info('ProspectCache - Opportunity DB search results', [\n 'account_id' => $account->getId(),\n 'contact_id' => $contactId,\n 'opportunity_id' => $opportunity?->getId(),\n ]);\n\n return $opportunity;\n }\n\n private function generateProspectTag(Configuration $configuration, string $identifier): string\n {\n return 'prospect:' . $configuration->getId() . ':' . $identifier;\n }\n\n private function getTags(Configuration $configuration, string $identifier): array\n {\n return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"bounds":{"left":0.70212764,"top":0.10055866,"width":0.019946808,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73105055,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8577493258770859059
|
7035618767607113532
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
Analyzing…
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\App;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Events\Crm\ProspectUpdated;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\Crm\CrmObjects\Validators\StaleRecordValidator;
use Jiminny\Services\Crm\Salesforce\OpportunityMatcher\MatchBusinessAccount;
use Psr\Log\LoggerInterface;
class ProspectCache
{
public const string PROSPECT_TYPE_EMAIL = 'email';
public const string PROSPECT_TYPE_PHONE = 'phone';
public const string PROSPECT_TYPE_DOMAIN = 'domain';
private const int TTL_SECONDS = 900;
private const int TTL_SECONDS_DEV = 30;
private const string DATADOG_STAT_NAME = 'jiminny.crm.prospect_cache_lookup';
private const string LOOKUP_RESULT_INTERNAL = 'internal';
private const string LOOKUP_RESULT_CACHE = 'cache';
private const string LOOKUP_RESULT_DB = 'db';
private const string LOOKUP_RESULT_MISS = 'miss';
public function __construct(
private readonly FindsProspectInterface $dbCache,
private readonly Repository $cache,
private readonly OpportunityRepository $opportunityRepository,
private readonly EmailHelper $emailHelper,
private readonly LoggerInterface $logger,
private readonly StaleRecordValidator $staleRecordValidator,
) {
}
/**
* @return null|array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* }
*/
public function findByProspectIdentifier(
Configuration $configuration,
?Profile $profile,
string $identifierType,
string $identifierValue,
?int $userId = null,
?SyncCrmEntitiesInterface $crmService = null
): ?array {
$cachedValue = $this->get($configuration, $identifierValue, $userId);
if ($cachedValue !== null) {
$this->sendDatadogStats(self::LOOKUP_RESULT_CACHE, $configuration->getProviderName());
return $cachedValue;
}
if ($identifierType == self::PROSPECT_TYPE_EMAIL
&& $this->emailHelper->isCompanyEmail($configuration->getTeam(), $identifierValue)
) {
// Set the cache to avoid querying the database for internal participants
$prospectData = [null, null, null, null, null, null];
$this->set($configuration, $identifierValue, $prospectData, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_INTERNAL, $configuration->getProviderName());
return $prospectData;
}
$dbCache = $this->dbCache->findProspect($configuration, [$identifierType => $identifierValue]);
if (empty(array_filter($dbCache))) {
$this->sendDatadogStats(self::LOOKUP_RESULT_MISS, $configuration->getProviderName());
return null;
}
$dbCache['contact'] = $this->staleRecordValidator->filterStale($dbCache['contact'], $crmService);
$dbCache['lead'] = $this->staleRecordValidator->filterStale($dbCache['lead'], $crmService);
if ($dbCache['contact'] instanceof Contact) {
$account = $dbCache['contact']->getAccount();
$dbCache['account'] = $account;
$opportunity = $this->findOpportunityInContactRoles(
$configuration,
$profile,
$dbCache['contact']->getId()
);
$opportunity = $this->staleRecordValidator->filterStale($opportunity, $crmService);
if ($opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Found opportunities by contact roles', [
'opportunity_id' => $opportunity->getId(),
]);
$dbCache['account'] = $opportunity->getAccount();
} elseif ($account instanceof Account) {
$opportunity = $this->getOpportunityFromDatabase(
configuration: $configuration,
account: $account,
contactId: $dbCache['contact']->getId(),
userId: $userId
);
}
$dbCache['opportunity'] = $opportunity;
$dbCache['account'] = $this->staleRecordValidator->filterStale($dbCache['account'], $crmService);
$dbCache['stage'] = $dbCache['opportunity']?->getStage();
}
/**
* @IMPORTANT The keys must always be in this exact order
*
* @var array{
* ?Lead,
* ?Account,
* ?Opportunity,
* ?Contact,
* ?Stage,
* string|null
* } $result
*/
$result = [
$dbCache['lead'] ?? null,
$dbCache['account'] ?? null,
$dbCache['opportunity'] ?? null,
$dbCache['contact'] ?? null,
$dbCache['stage'] ?? null,
null,
];
$this->set($configuration, $identifierValue, $result, $userId);
$this->sendDatadogStats(self::LOOKUP_RESULT_DB, $configuration->getProviderName());
return $result;
}
public function findDomainMatch(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
$cachedValue = $this->get(
configuration: $configuration,
identifier: $identifier,
userId: $userId
);
if ($cachedValue !== null) {
$this->sendDatadogStats(
self::LOOKUP_RESULT_CACHE,
$configuration->getProviderName()
);
return $cachedValue;
}
// Log cache miss
$this->logger->info('[Prospect match] Cache miss', [
'identifier_type' => self::PROSPECT_TYPE_DOMAIN,
'identifier' => $identifier,
'crm' => $configuration->getProviderName(),
]);
// not in the cache
return null;
}
public function set(Configuration $configuration, string $identifier, array $prospectData, ?int $userId = null): void
{
$this->cache->tags($this->getTags($configuration, $identifier))->put(
$this->generateKey($configuration, $identifier, $userId),
$prospectData,
$this->getCacheTtl()
);
}
public function handleProspectUpdated(ProspectUpdated $event): void
{
$prospect = $event->getProspect();
$configuration = $prospect->getCrmConfiguration();
if ($configuration === null) {
return;
}
if ($prospect->getEmail() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getEmail())])->flush();
}
if ($prospect->getPhone() !== null) {
$normalizedPhone = $this->normalizePhoneNumber($prospect->getPhone());
$this->cache->tags([$this->generateProspectTag($configuration, $normalizedPhone)])->flush();
}
if ($prospect->getName() !== null) {
$this->cache->tags([$this->generateProspectTag($configuration, $prospect->getName())])->flush();
}
}
public function normalizePhoneNumber(string $phone): string
{
// Remove all non-digit characters first
$digitsOnly = preg_replace('/[^\d]/', '', $phone);
// Remove a single leading zero if present
$digitsOnly = ltrim($digitsOnly, '0');
// Add E.164 prefix
return '+' . $digitsOnly;
}
private function get(Configuration $configuration, string $identifier, ?int $userId = null): ?array
{
return $this->cache->tags($this->getTags($configuration, $identifier))
->get($this->generateKey($configuration, $identifier, $userId));
}
private function generateKey(Configuration $configuration, string $identifier, ?int $userId = null): string
{
$keySuffix = $userId === null ? '' : ':user:' . $userId;
return hash('sha256', 'crm:' . $configuration->getId() . ':prospect:' . $identifier . $keySuffix);
}
private function sendDatadogStats(string $result, string $crm): void
{
Datadog::increment(self::DATADOG_STAT_NAME, 1, [
'result' => $result,
'crm' => $crm,
]);
}
private function getCacheTtl(): int
{
if (! App::environment('production', 'production-eu')) {
return self::TTL_SECONDS_DEV;
}
return self::TTL_SECONDS;
}
private function findOpportunityInContactRoles(
Configuration $configuration,
?Profile $profile,
int $contactId
): ?Opportunity {
$contactRoleRepository = app(ContactRoleRepository::class);
$contactRoles = $contactRoleRepository->getByCrmContactId(
$configuration,
$contactId,
);
if (! $contactRoles->isEmpty()) {
$opportunityId = app(MatchBusinessAccount::class)->resolve(
$contactRoles,
$profile?->getCrmProviderId(),
);
$opportunity = $this->opportunityRepository->find($opportunityId);
}
return $opportunity ?? null;
}
private function getOpportunityFromDatabase(
Configuration $configuration,
Account $account,
int $contactId,
?int $userId = null
): ?Opportunity {
$opportunity = null;
if ($userId) {
$this->logger->info('ProspectCache - Searching DB for opportunity by owner', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'owner_id' => $userId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityOwner(
$configuration,
$account,
$userId,
$contactId
);
}
if (! $opportunity instanceof Opportunity) {
$this->logger->info('ProspectCache - Fallback DB opportunity search', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
]);
$opportunity = $this->opportunityRepository->findOneByAccountAndOpportunityAssignmentRule(
$configuration,
$account,
$contactId
);
}
$this->logger->info('ProspectCache - Opportunity DB search results', [
'account_id' => $account->getId(),
'contact_id' => $contactId,
'opportunity_id' => $opportunity?->getId(),
]);
return $opportunity;
}
private function generateProspectTag(Configuration $configuration, string $identifier): string
{
return 'prospect:' . $configuration->getId() . ':' . $identifier;
}
private function getTags(Configuration $configuration, string $identifier): array
{
return ['prospect_cache', $this->generateProspectTag($configuration, $identifier)];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42576
|
1560
|
8
|
2026-05-14T11:42:53.520617+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778758973520_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybookCreated.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20904-fix-update-es-on Project: faVsco.js, menu
JY-20904-fix-update-es-on-activity-command, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Events\Playbooks;
use Illuminate\Queue\SerializesModels;
use Jiminny\Models\Playbook;
use Jiminny\Models\User;
class PlaybookCreated
{
use SerializesModels;
/**
* The playbook instance.
*
* @var Playbook
*/
public Playbook $playbook;
/**
* The user who created the playbook.
*
* @var User
*/
public User $user;
/**
* Create a new event instance.
*/
public function __construct(Playbook $playbook, User $user)
{
$this->playbook = $playbook;
$this->user = $user;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20904-fix-update-es-on-activity-command, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11070479,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20904-fix-update-es-on-activity-command","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Events\\Playbooks;\n\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\User;\n\nclass PlaybookCreated\n{\n use SerializesModels;\n\n /**\n * The playbook instance.\n *\n * @var Playbook\n */\n public Playbook $playbook;\n\n /**\n * The user who created the playbook.\n *\n * @var User\n */\n public User $user;\n\n /**\n * Create a new event instance.\n */\n public function __construct(Playbook $playbook, User $user)\n {\n $this->playbook = $playbook;\n $this->user = $user;\n }\n}","depth":4,"bounds":{"left":0.11968085,"top":0.22106944,"width":0.28823137,"height":0.7581804},"on_screen":true,"lines":[{"char_start":0,"char_count":6,"bounds":{"left":0.11968085,"top":0.0,"width":0.012965426,"height":0.014365523}},{"char_start":7,"char_count":36,"bounds":{"left":0.11968085,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":44,"char_count":39,"bounds":{"left":0.11968085,"top":0.009577015,"width":0.010305851,"height":0.014365523}},{"char_start":83,"char_count":29,"bounds":{"left":0.1299867,"top":0.009577015,"width":0.0026595744,"height":0.014365523}},{"char_start":112,"char_count":25,"bounds":{"left":0.1299867,"top":0.009577015,"width":0.0076462766,"height":0.014365523}},{"char_start":138,"char_count":22,"bounds":{"left":0.11968085,"top":0.044692736,"width":0.05418883,"height":0.014365523}},{"char_start":160,"char_count":2,"bounds":{"left":0.11968085,"top":0.0622506,"width":0.0026595744,"height":0.014365523}},{"char_start":162,"char_count":26,"bounds":{"left":0.11968085,"top":0.07980846,"width":0.06482713,"height":0.014365523}},{"char_start":189,"char_count":8,"bounds":{"left":0.11968085,"top":0.114924185,"width":0.017952127,"height":0.014365523}},{"char_start":197,"char_count":30,"bounds":{"left":0.11968085,"top":0.13248204,"width":0.07513298,"height":0.014365523}},{"char_start":227,"char_count":7,"bounds":{"left":0.11968085,"top":0.15003991,"width":0.015292553,"height":0.014365523}},{"char_start":234,"char_count":21,"bounds":{"left":0.11968085,"top":0.16759777,"width":0.051861703,"height":0.014365523}},{"char_start":255,"char_count":8,"bounds":{"left":0.11968085,"top":0.18515563,"width":0.017952127,"height":0.014365523}},{"char_start":263,"char_count":31,"bounds":{"left":0.11968085,"top":0.20271349,"width":0.077792555,"height":0.014365523}},{"char_start":295,"char_count":8,"bounds":{"left":0.11968085,"top":0.23782921,"width":0.017952127,"height":0.014365523}},{"char_start":303,"char_count":42,"bounds":{"left":0.11968085,"top":0.25538707,"width":0.10605053,"height":0.014365523}},{"char_start":345,"char_count":7,"bounds":{"left":0.11968085,"top":0.27294493,"width":0.015292553,"height":0.014365523}},{"char_start":352,"char_count":17,"bounds":{"left":0.11968085,"top":0.2905028,"width":0.041223403,"height":0.014365523}},{"char_start":369,"char_count":8,"bounds":{"left":0.11968085,"top":0.30806065,"width":0.017952127,"height":0.014365523}},{"char_start":377,"char_count":23,"bounds":{"left":0.11968085,"top":0.3256185,"width":0.056848403,"height":0.014365523}},{"char_start":401,"char_count":8,"bounds":{"left":0.11968085,"top":0.36073422,"width":0.017952127,"height":0.014365523}},{"char_start":409,"char_count":36,"bounds":{"left":0.11968085,"top":0.3782921,"width":0.09075798,"height":0.014365523}},{"char_start":445,"char_count":8,"bounds":{"left":0.11968085,"top":0.39584997,"width":0.017952127,"height":0.014365523}},{"char_start":453,"char_count":143,"bounds":{"left":0.11968085,"top":0.41340783,"width":0.0026595744,"height":0.10215483}}],"value":"<?php\n\nnamespace Jiminny\\Events\\Playbooks;\n\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\User;\n\nclass PlaybookCreated\n{\n use SerializesModels;\n\n /**\n * The playbook instance.\n *\n * @var Playbook\n */\n public Playbook $playbook;\n\n /**\n * The user who created the playbook.\n *\n * @var User\n */\n public User $user;\n\n /**\n * Create a new event instance.\n */\n public function __construct(Playbook $playbook, User $user)\n {\n $this->playbook = $playbook;\n $this->user = $user;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.40957448,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.41821808,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.42918882,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.43783244,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.44647607,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.4574468,"top":0.123703115,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7349379181401574198
|
-8886576135686337790
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20904-fix-update-es-on Project: faVsco.js, menu
JY-20904-fix-update-es-on-activity-command, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Events\Playbooks;
use Illuminate\Queue\SerializesModels;
use Jiminny\Models\Playbook;
use Jiminny\Models\User;
class PlaybookCreated
{
use SerializesModels;
/**
* The playbook instance.
*
* @var Playbook
*/
public Playbook $playbook;
/**
* The user who created the playbook.
*
* @var User
*/
public User $user;
/**
* Create a new event instance.
*/
public function __construct(Playbook $playbook, User $user)
{
$this->playbook = $playbook;
$this->user = $user;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42575
|
1559
|
7
|
2026-05-14T11:42:51.286766+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778758971286_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybookCreated.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20904-fix-update-es-on Project: faVsco.js, menu
JY-20904-fix-update-es-on-activity-command, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Events\Playbooks;
use Illuminate\Queue\SerializesModels;
use Jiminny\Models\Playbook;
use Jiminny\Models\User;
class PlaybookCreated
{
use SerializesModels;
/**
* The playbook instance.
*
* @var Playbook
*/
public Playbook $playbook;
/**
* The user who created the playbook.
*
* @var User
*/
public User $user;
/**
* Create a new event instance.
*/
public function __construct(Playbook $playbook, User $user)
{
$this->playbook = $playbook;
$this->user = $user;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
30
9
27
3
106
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM team_features where team_id = 1;
SELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922
SELECT * FROM users WHERE team_id = 340; # 12015
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 340
and sa.provider = 'salesforce';
# and sa.provider = 'salesloft';
select * from crm_fields where crm_configuration_id = 270 and object_type = 'event';
# 125558 - Event Type - Event_Type__c
# 125552 - Event Status - Event_Status__c
SELECT * FROM sidekick_settings WHERE team_id = 340;
SELECT * FROM crm_field_values WHERE crm_field_id in (125552);
select * from activities where crm_configuration_id = 270
and type = 'conference' and crm_provider_id IS NOT NULL
and actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;
SELECT * FROM activities WHERE id = 20871677;
SELECT * FROM crm_field_data WHERE activity_id = 20871677;
select * from crm_layouts where crm_configuration_id = 270;
select * from crm_layout_entities where crm_layout_id in (886,887);
SELECT * FROM crm_configurations WHERE id = 270;
select * from playbooks where team_id = 340; # 1514
select * from groups where team_id = 340;
SELECT * FROM crm_fields WHERE id IN (125393, 125401);
select g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g
join playbooks p on g.playbook_id = p.id
join crm_fields f on p.activity_field_id = f.id
where g.team_id = 340;
SELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716
select * from crm_field_data where object_id = 20448716;
select * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008
select * from opportunities where team_id = 343;
select * from opportunities where team_id = 343 and crm_provider_id = '18099102526';
select * from opportunities where team_id = 343 and account_id = 945217482;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 343
and sa.provider = 'hubspot';
select * from accounts where team_id = 343 order by name asc;
select * from stages where crm_configuration_id = 273 and type = 'opportunity';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143
SELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;
SELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';
SELECT * FROM activities WHERE id = 20717903;
select * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 353
and sa.provider = 'salesforce';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, [EMAIL]
SELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;
# id: 20940638, user: 12022, contact: 5305871
SELECT * FROM activity_summary_logs WHERE activity_id = 20940638;
select * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 345
and sa.provider = 'hubspot';
select * from users where team_id = 345 and id = 12022;
SELECT * FROM crm_profiles WHERE user_id = 12022;
SELECT * FROM participants WHERE activity_id = 20940638;
SELECT * FROM users u
JOIN crm_profiles cp ON u.id = cp.user_id
WHERE u.team_id = 345;
select * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871
select * from team_features where team_id = 345;
SELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197
SELECT * FROM participants WHERE activity_id = 20897406;
SELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912
SELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';
SELECT * FROM activities WHERE id = 20946641;
SELECT * FROM crm_profiles WHERE user_id = 10211;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, [EMAIL]
SELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';
select * from stages where crm_configuration_id = 97 and type = 'opportunity';
select * from opportunities where team_id = 120;
select * from crm_configurations crm join teams t on crm.id = t.crm_id
where 1=1
AND t.current_billing_plan IS NOT NULL
AND crm.auto_sync_activity = 0
and crm.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,[EMAIL]
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 270
and sa.provider = 'salesforce';
SELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956
SELECT * FROM crm_profiles WHERE user_id = 11446;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, [EMAIL]
select * from playbooks where team_id = 372;
select * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340
SELECT * FROM crm_field_values WHERE crm_field_id = 141340;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 372
and sa.provider = 'salesforce';
select * from crm_profiles where crm_configuration_id = 300;
SELECT * FROM crm_configurations WHERE team_id = 372;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,[EMAIL]
SELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756
select * from crm_field_data where object_id = 3207756;
SELECT * FROM crm_fields WHERE id = 111834;
select f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value
FROM crm_fields f
JOIN crm_field_data fd ON f.id = fd.crm_field_id
WHERE f.crm_configuration_id = 242
AND f.object_type = 'opportunity'
AND fd.object_id IN (3207756)
ORDER BY fd.object_id, fd.updated_at;
SELECT * FROM crm_configurations WHERE auto_connect = 1;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,[EMAIL]
select * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id
where g.team_id = 187;
select * from `groups` where team_id = 187;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 187
and sa.provider = 'salesforce';
# Destination - 98870 - Destination__c
# Stage - 79014 - StageName
# Land Arrangement - 98856 - Land_Arrangement__c
# Flight - 98848 - Flight__c
# Last activity date - 98812 - LastActivityDate
# Last modified date - 98809 - LastModifiedDate
# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c
# next call - 98864 - Next_Call__c
select * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 209;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;
select * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';
select * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;
select * from activities where opportunity_id = 3538248;
SELECT * FROM crm_profiles WHERE user_id = 8150;
select * from deal_risks where opportunity_id = 3538248;
select * from teams where crm_id IS NULL;
SELECT opp.id AS opportunity_id,
u.group_id AS group_id,
MAX(
CASE
WHEN a.type IN ("sms-inbound", "sms-outbound") THEN a.created_at
ELSE a.actual_end_time
END) as last_date
FROM opportunities opp
left join activities a on a.opportunity_id = opp.id
inner join users u on opp.user_id = u.id
where opp.user_id IN (9951)
AND opp.is_closed = 0
and a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL
group by opp.id;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,[EMAIL]
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 343
and sa.provider = 'hubspot';
SELECT * FROM crm_profiles WHERE crm_configuration_id = 301;
SELECT * FROM contacts WHERE id = 6612363;
SELECT * FROM accounts WHERE id = 4235676;
SELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;
select * from opportunity_stages where opportunity_id = 4503759;
# SELECT * FROM opportunities WHERE id = 4569937;
select * from activities where crm_configuration_id = 301;
SELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370
SELECT * FROM participants WHERE activity_id = 26330370;
SELECT * FROM teams WHERE id = 375;
select * from playbooks where team_id = 375;
select * from stages where crm_configuration_id = 301 and type = 'opportunity';
select * from teams;
select * from contact_roles;
SELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';
select * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;
SELECT * FROM crm_field_data WHERE object_id = 3771706;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 343
and sa.provider = 'hubspot';
SELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'
and crm_provider_id LIKE "%traffic_light%";
SELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);
SELECT fd.* FROM opportunities o
JOIN crm_field_data fd ON o.id = fd.object_id
WHERE o.team_id = 343
# and o.user_id IS NOT NULL
and fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)
and fd.value != ''
order by value desc
# group by o.id
;
SELECT * FROM opportunities WHERE id = 3769843;
SELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, [EMAIL]
SELECT * FROM crm_layouts WHERE crm_configuration_id = 209;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,[EMAIL]
SELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839
SELECT * FROM opportunities WHERE id = 3855992;
SELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988
SELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894
SELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';
select * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507
SELECT * FROM crm_field_data WHERE object_id = 5874411;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 379
and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, [EMAIL]
SELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, [EMAIL]
SELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793
select * from generic_ai_prompts where subject_id = 3537793;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, [EMAIL]
SELECT * FROM crm_configurations WHERE id = 97;
SELECT * FROM crm_layouts WHERE crm_configuration_id = 97;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;
SELECT * FROM crm_fields WHERE id = 32682;
select cfd.value, o.* from opportunities o
join crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682
where team_id = 120
and cfd.value != ''
;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 120
and sa.provider = 'salesforce';
select * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';
SELECT * FROM crm_field_data WHERE object_id = 2313439;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE id = 410;
SELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';
select * from scorecards where team_id = 410;
select * from scorecard_rules;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, [EMAIL]
select * from activities a
join opportunities o on a.opportunity_id = o.id
join users u on o.user_id = u.id
where a.crm_configuration_id = 177 and a.type LIKE '%email-out%'
# and a.actual_end_time > '2024-12-16 00:00:00'
# and o.remotely_created_at > '2024-12-01 00:00:00'
# and u.group_id = 1014
and u.id = 9021
order by a.id desc;
SELECT * FROM opportunities WHERE id in (3981384,4017346);
SELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);
select * from users where id = 9021;
select * from inboxes where user_id = 9021;
select * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';
select * from email_messages where team_id = 220
and orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'
and subject LIKE '%Personal%'
# and 'from' = '[EMAIL]'
;
select * from activities a
join opportunities o on a.opportunity_id = o.id
where a.user_id = 9021 and a.type LIKE '%email-out%'
and a.actual_end_time > '2024-12-18 00:00:00'
and o.user_id IS NOT NULL
and o.remotely_created_at > '2024-12-01 00:00:00'
order by a.id desc;
SELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;
select * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;
select * from team_settings where name IN ('useCloseDate');
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, [EMAIL]
SELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 104
and sa.provider = 'hubspot';
select * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'
select * from teams where crm_id IS NULL;
select t.name as 'team', u.name as 'owner', u.email, u.phone
from teams t
join activity_providers ap on t.id = ap.team_id
join users u on t.owner_id = u.id
where 1=1
and t.status = 'active'
and ap.is_enabled = 1
# and u.status = 1
and ap.provider = 'ms-teams';
select * from crm_configurations where provider = 'bullhorn'; # 344
SELECT * FROM teams WHERE id = 442; # 14293
select * from users where team_id = 442;
select * from social_accounts sa where sa.sociable_id = 14293;
select * from invitations where team_id = 442;
# [PASSWORD_DOTS]
SELECT * FROM users WHERE email LIKE '%[EMAIL]%'; # 14022
SELECT * FROM teams WHERE id = 429;
select * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);
select * from activities where opportunity_id in (4340436,4353519);
select * from transcription where activity_id IN (25630961,25381771);
select * from generic_ai_prompts where subject_id IN (4353519);
SELECT
a.id as activity_id,
a.opportunity_id,
a.type as activity_type,
a.language,
CONCAT(a.title, a.description) AS mail_content,
e.from AS mail_from,
e.to AS mail_to,
e.subject AS mail_subject,
e.body AS mail_body,
p.type as prompt_type,
p.status as prompt_status,
p.content AS prompt_content,
a.actual_start_time as created_at
FROM activities a
LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL
LEFT JOIN email_messages e ON a.id = e.activity_id
WHERE a.actual_start_time > '2024-01-01 00:00:00'
AND a.opportunity_id IN (4353519)
AND a.status IN ('completed', 'received', 'delivered')
AND a.deleted_at IS NULL
AND a.type NOT IN ('sms-inbound', 'sms-outbound')
ORDER BY a.opportunity_id ASC, a.id ASC;
SELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293
SELECT * FROM teams WHERE id = 442;
SELECT * FROM crm_configurations WHERE id = 344;
select * from team_features where team_id = 442;
select * from groups where team_id = 442;
select * from playbooks where team_id = 442;
select * from playbook_categories where playbook_id = 1729;
select * from crm_fields where crm_configuration_id = 344 and id = 172024;
SELECT * FROM crm_field_values WHERE crm_field_id = 172024;
select * from crm_layouts where crm_configuration_id = 344;
select * from playbook_layouts where playbook_id = 1729;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444
select s.*
# , s.sent_at, u.name, a.*
from activity_summary_logs s
inner join activities a on a.id = s.activity_id
inner join users u on u.id = a.user_id
where a.crm_configuration_id = 356
and s.sent_at > date_sub(now(), interval 60 day)
order by a.actual_end_time desc;
select * from activities a
# inner join activity_summary_logs s on s.activity_id = a.id
where a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)
# and a.crm_provider_id is not null
# and provider <> 'ringcentral'
and status = 'completed'
order by a.actual_end_time desc;
select * from teams order by id desc; # 17328, 32, 17830, [EMAIL]
SELECT * FROM users;
SELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active
SELECT * FROM teams WHERE id = 260;
select * from team_settings where team_id = 260;
select * from crm_configurations where team_id = 260;
SELECT * FROM crm_layouts WHERE crm_configuration_id = 356;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;
select * from accounts where crm_configuration_id = 221 order by id desc; # 7000
select * from leads where crm_configuration_id = 221 order by id desc; # 0
select * from contacts where crm_configuration_id = 221 order by id desc; # 200 000
select * from opportunities where crm_configuration_id = 221 order by id desc; # 0
select * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23
select * from crm_fields where crm_configuration_id = 221;
select * from crm_field_values where crm_field_id = 5302 order by id desc;
select * from crm_layouts where crm_configuration_id = 221 order by id desc;
select * from stages where crm_configuration_id = 221 order by id desc;
select * from accounts where crm_configuration_id = 356 order by id desc; # 7000
select * from leads where crm_configuration_id = 356 order by id desc; # 0
select * from contacts where crm_configuration_id = 356 order by id desc; # 200 000
select * from opportunities where crm_configuration_id = 356 order by id desc; # 0
select * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23
select * from crm_fields where crm_configuration_id = 356;
select * from crm_field_values where crm_field_id = 5302 order by id desc;
select * from crm_layouts where crm_configuration_id = 356 order by id desc;
select * from stages where crm_configuration_id = 356 order by id desc;
select * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)
select * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)
select * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4
select ce.* from calendars c
join users u on c.user_id = u.id
join calendar_events ce on c.id = ce.calendar_id
where u.team_id = 260
and (ce.start_time > '2025-02-21 00:00:00')
;
# calendar events 1207
#
select * from opportunities where team_id = 260;
SELECT * FROM crm_field_data WHERE object_id = 4696496;
select * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;
select * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')
# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0
and created_at > '2024-03-01 00:00:00'
order by id desc; # 880 000, ringcentral, avaya
SELECT * FROM participants WHERE activity_id = 26371744;
# all activities 942 000 +
# conference 7385 - scheduled 984 - external 343
select * from activities where id = 26321812;
select * from participants where activity_id = 26321812;
select * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);
select * from leads where id in (720428,689175,731546,645866,621037);
select * from users where id = 13841;
select * from opportunities where user_id = 9541;
select * from stages where id = 15900;
select * from accounts where
# id IN (4160055,5053725,4965303,4896434)
id in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)
;
select * from activities where id = 26654935;
SELECT * FROM opportunities WHERE id = 4803458;
SELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;
SELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time
FROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);
SELECT DISTINCT
o.id, o.stage_id, s.name, a.title,
a.*
FROM activities a
# INNER JOIN tracks t ON a.id = t.activity_id
INNER JOIN users u ON a.user_id = u.id
INNER JOIN teams team ON u.team_id = team.id
INNER JOIN groups g ON u.group_id = g.id
INNER JOIN opportunities o ON a.opportunity_id = o.id
INNER JOIN stages s ON o.stage_id = s.id
WHERE
a.crm_configuration_id = 356
AND a.status IN ('completed', 'failed')
AND a.recording_state != 'stopped'
# and a.user_id = 13841
AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')
AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')
AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')
AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
# AND t.type IN ('audio', 'video')
AND (
(a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')
OR
(
a.actual_start_time IS NULL
AND a.type IN ('sms-outbound', 'sms-inbound')
AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'
)
)
AND (
a.is_private = 0
OR (
a.is_private = 1
AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')
)
)
AND (
# s.id = 15900
s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')
OR s.uuid IS NULL -- Include records without opportunity stage
)
ORDER BY a.actual_end_time DESC;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, [EMAIL]
SELECT * FROM users WHERE team_id = 190;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 190
and sa.provider = 'hubspot';
select * from role_user where user_id = 8474;
select * from crm_configurations where provider = 'bullhorn';
SELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;
SELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;
SELECT * FROM opportunities WHERE id = 4732493;
select * from activities where opportunity_id = 4732493;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE id = 443; # 358, 14315, [EMAIL]
SELECT * FROM opportunities WHERE team_id = 443;
SELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id
FROM activities AS a
JOIN stages AS s ON a.stage_id = s.id
JOIN users AS u ON u.id = a.user_id
JOIN teams AS t ON t.id = s.team_id
WHERE u.team_id <> s.team_id and t.id > 135;
SELECT
crm_configuration_id,
crm_provider_id,
COUNT(*) as duplicate_count,
GROUP_CONCAT(id) as stage_ids,
GROUP_CONCAT(name) as stage_names
FROM stages
GROUP BY crm_configuration_id, crm_provider_id
HAVING COUNT(*) > 1
ORDER BY duplicate_count DESC;
select * from stages where id IN (14898,14907);
select * from business_processes;
SELECT *
FROM crm_configurations
WHERE team_id IN (
SELECT team_id
FROM crm_configurations
GROUP BY team_id
HAVING COUNT(*) > 1
)
ORDER BY team_id;
SELECT *
FROM teams
WHERE crm_id IN (
SELECT crm_id
FROM teams
GROUP BY crm_id
HAVING COUNT(*) > 1
)
ORDER BY crm_id;
# [PASSWORD_DOTS]
select * from crm_configurations where provider = 'integration-app';
SELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 [EMAIL]
select * from activities where crm_configuration_id = 358 order by actual_end_time desc;
select id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;
select * from team_features where team_id = 358;
select * from activity_summary_logs;
select * from teams where id = 406;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, [EMAIL]
select * from activities where crm_configuration_id = 202 order by actual_end_time desc;
SELECT * FROM users where id = 14637;
SELECT * FROM teams where id = 267;
SELECT * FROM groups where id = 1118;
select g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a
inner join users u on u.id = a.user_id
inner join groups g on g.id = u.group_id
where a.crm_configuration_id = 202
and a.is_internal = 0
and (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')
and a.type = 'conference'
and a.status != 'completed'
and a.external_id is not null
order by a.scheduled_start_time desc;
SELECT * FROM activities
WHERE crm_configuration_id = 202
AND status IN ('completed', 'failed')
AND recording_state != 'stopped'
AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
AND (is_private = 0 OR user_id = 14637)
AND (
(
actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'
) OR (
actual_start_time IS NULL
AND type IN ('sms-outbound', 'sms-inbound')
AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'
)
)
AND NOT EXISTS (
SELECT 1
FROM tracks
WHERE
tracks.activity_id = activities.id
AND tracks.type IN ('audio', 'video')
)
ORDER BY actual_end_time DESC;
SELECT DISTINCT
a.*
FROM activities a
INNER JOIN tracks t ON a.id = t.activity_id
INNER JOIN users u ON a.user_id = u.id
INNER JOIN teams team ON u.team_id = team.id
WHERE
a.crm_configuration_id = 202
AND a.status IN ('completed', 'failed')
AND a.recording_state != 'stopped'
# and a.user_id = 14637
AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
# AND t.type IN ('audio', 'video')
AND (
(a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')
OR
(
a.actual_start_time IS NULL
AND a.type IN ('sms-outbound', 'sms-inbound')
AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'
)
)
AND (
a.is_private = 0
OR (
a.is_private = 1
AND a.user_id = 14637
)
)
ORDER BY a.actual_end_time DESC
;
SELECT DISTINCT a.*
FROM activities a
INNER JOIN users u ON a.user_id = u.id
INNER JOIN teams t ON u.team_id = t.id
# INNER JOIN tracks tr ON a.id = tr.activity_id
# INNER JOIN groups g ON u.group_id = g.id
WHERE 1=1
AND t.id = 267
# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')
AND a.status IN ('completed', 'failed')
AND a.recording_state != 'stopped'
AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
# AND tr.type NOT IN ('audio', 'video')
AND (
a.is_private = 0
OR a.user_id = 14637
)
AND (
(a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')
OR (
a.actual_start_time IS NULL
AND a.type IN ('sms-outbound', 'sms-inbound')
AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'
)
)
# and NOT EXISTS (
# SELECT 1
# FROM tracks t
# WHERE t.activity_id = a.id
# AND t.type IN ('audio', 'video')
# )
ORDER BY a.actual_end_time DESC;
SELECT * FROM tracks WHERE activity_id = 26485995;
select a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a
inner join users u on u.id = a.user_id
where a.crm_configuration_id = 202
# and a.is_internal = 0
and (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')
and a.type IN ("softphone","softphone-inbound","conference","sms-inbound")
and a.status IN ('completed', 'failed')
# and a.external_id is not null
order by a.actual_end_time desc;
select * from activities a where a.crm_configuration_id = 202
and a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'
# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
select g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a
inner join users u on u.id = a.user_id
inner join groups g on g.id = u.group_id
where a.crm_configuration_id = 202
and a.is_internal = 0
and (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')
and a.type = 'conference'
and a.status != 'completed'
and a.external_id is not null
order by a.scheduled_start_time desc;
SELECT * FROM teams WHERE name LIKE '%Tourlane%';
SELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';
SELECT * FROM crm_field_data WHERE crm_field_id = 98809;
select * from users where status = 1 AND timezone = 'MDT';
select * from opportunities where id = 3769814;
select * from deal_risks where opportunity_id = 3769814;
select cp.* from crm_profiles cp
join users u on cp.user_id = u.id
join crm_configurations crm on cp.crm_configuration_id = crm.id
where crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';
select * from crm_fields where id = 154575;
select * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';
SELECT * FROM teams WHERE id = 176; # crm 148
select * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;
select * from activity_providers where provider = 'amazon-connect';
select * from crm_fields cf
join crm_configurations crm on crm.id = cf.crm_configuration_id
where crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');
# [PASSWORD_DOTS]
SELECT * FROM users WHERE id IN (15415, 15418);
SELECT * FROM groups WHERE id IN (1805,1806);
SELECT * FROM playbooks WHERE id = 1860;
SELECT * FROM playbook_categories WHERE id = 38634;
SELECT * FROM crm_fields WHERE id = 189962;
SELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 [EMAIL]
SELECT * FROM crm_profiles WHERE user_id = 15415;
SELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';
select * from sidekick_settings where team_id = 472;
SELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418
SELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415
SELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415
SELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, [EMAIL]
select * from crm_configurations where id = 218;
SELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765
SELECT * FROM users WHERE id IN (13232, 13230);
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 109
and sa.provider = 'salesforce';
0057R00000EPL5HQAX Inez Ekblad
1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur
SELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);
############################################################################################
SELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT
SELECT * FROM crm_field_data WHERE activity_id = 28655939;
SELECT * FROM crm_fields WHERE id IN (94491,94493,94498);
SELECT * FROM users WHERE id = 13658;
SELECT * FROM teams WHERE id = 109;
SELECT * FROM crm_configurations WHERE id = 218;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 109
and sa.provider = 'salesforce';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, [EMAIL]
SELECT * FROM stages WHERE crm_configuration_id = 390;
select * from business_processes where team_id = 481 and crm_configuration_id = 390;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 481
and sa.provider = 'salesforce';
SELECT * FROM users WHERE id = 15780; # team 462
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 462
and sa.provider = 'hubspot';
select * from teams where id = 495;
SELECT * FROM users WHERE id = 15794;
select * from social_accounts where sociable_id = 15794;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752
SELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794
SELECT * FROM activities WHERE crm_configuration_id = 407
and status = 'completed' and type = 'conference'
order by id desc;
select ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id
join permission_role pr on pr.role_id = ru.role_id
join permissions p on p.id = pr.permission_id
where team_id = 495 and p.name IN ('dial');
select * from permission_role;
select * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;
SELECT * FROM activities WHERE id = 29512773;
SELECT * FROM activities WHERE id IN (29042721,28991325,29002874);
SELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id
where a.crm_configuration_id = 407
# and a.id IN (29042721,28991325,29002874);
SELECT * FROM users WHERE id = 15794;
SELECT * FROM users WHERE team_id = 495;
SELECT * FROM social_accounts WHERE sociable_id = 15794;
SELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';
SELECT * FROM contacts WHERE team_id = 495;
SELECT * FROM leads WHERE team_id = 495;
SELECT * FROM accounts WHERE team_id = 495;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 407;
SELECT * FROM crm_fields WHERE crm_configuration_id = 407;
SELECT * FROM crm_configurations WHERE id = 407;
SELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'
and user_id IS NOT NULL and is_closed = 1 and is_won = 1;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103
SELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064
SELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 325
and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085
SELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733
SELECT * FROM activity_summary_logs where activity_id = 28719733;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444
SELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';
SELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630
select * from activities where crm_configuration_id = 356 and lead_id = 841732;
SELECT * from activity_summary_logs al join activities a on a.id = al.activity_id
where a.crm_configuration_id = 356;
select * from activities where crm_configuration_id = 356
and actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'
order by id desc;
select * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;
select * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;
select * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;
select * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;
select * from team_features where team_id = 260;
select * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);
SELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;
select * from crm_fields;
select * from crm_layout_entities;
SELECT * FROM teams WHERE name LIKE '%Optable%';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969
SELECT * FROM crm_configurations WHERE id = 218;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 109
and sa.provider = 'salesforce';
SELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939
SELECT * FROM crm_field_data WHERE activity_id = 28655939;
SELECT * FROM crm_fields WHERE id in (94491,94493,94498);
select * from teams where crm_id IS NULL;
SELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;
# [PASSWORD_DOTS]
select * from team_domains where team_id = 399;
SELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207
select * from calendar_events where id = 5163781;
SELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896
SELECT * FROM participants WHERE activity_id = 29443896;
select * from contacts where crm_configuration_id = 318 and email = '[EMAIL]';
select * from leads where crm_configuration_id = 318 and email = '[EMAIL]';
select * from activities where user_id = 14937 order by created_at ;
select * from users where id = 14937;
select * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';
select * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';
select * from activities a join participants p on a.id = p.activity_id
where crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';
# [PASSWORD_DOTS]
SELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';
SELECT * FROM opportunities WHERE team_id = 379 order by id desc;
SELECT * FROM teams WHERE id = 379;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 379 and sociable_id = 13852
and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE id = 307;
SELECT * FROM crm_layouts WHERE crm_configuration_id = 307;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;
SELECT * FROM crm_fields WHERE crm_configuration_id = 307
and id IN (144750,144855,145158,155227);
SELECT * FROM activities;
select * from activities
where created_at > '2025-07-01 00:00:00'
# and created_at < '2025-08-01 00:00:00'
and type not in ('email-outbound', 'email-inbound')
and account_id is null
and contact_id is null
and lead_id is null
and opportunity_id is not null
;
SELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);
SELECT * FROM crm_configurations WHERE id in (335,301,200);
select * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';
SELECT * FROM teams WHERE name LIKE '%Resights%';
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_configurations where provider = 'bullhorn'; # 344
select * from teams where id IN (442);
select * from activities
where crm_configuration_id = 177
and provider = 'amazon-connect'
order by id desc;
# and source <> 'gong';
select * from activity_providers where provider = 'amazon-connect';
SELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;
select * from crm_configurations where store_transcript = 1;
SELECT * FROM teams WHERE id IN (80);
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 277
and sa.provider = 'salesforce';
select * from activities where crm_configuration_id = 213 and account_id = 2511502;
select * from crm_configurations where id = 213;
SELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604
SELECT * FROM participants WHERE activity_id = 33981604;
SELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 431
and sa.provider = 'salesforce';
SELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223
select * from activity_summary_logs where activity_id = 33997223;
select * from activity_notes where activity_id = 33997223;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Abode%';
select * from features;
select * from teams t
where t.status = 'active'
and id NOT IN (select team_id from team_features where feature_id = 9)
;
select * from playbook_layouts where playbook_id = 1725;
SELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473
select * from teams where id = 318;
select * from crm_configurations where team_id = 318;
select * from playbooks where team_id = 318;
SELECT * FROM crm_layouts where crm_configuration_id = 381;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;
SELECT * FROM crm_fields WHERE id IN (192938,192936,192939);
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;
SELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);
SELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;
SELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);
SELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124
select * from crm_layouts where crm_configuration_id = 281;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;
select * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);
select * from opportunities where crm_configuration_id = 281;
SELECT * FROM activities WHERE id IN (34211315, 34130075);
SELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);
select cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd
join crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id
join crm_fields cf on cle.crm_field_id = cf.id
where cf.deleted_at IS NOT NULL
GROUP BY cle.id, cf.id;
select * from crm_layouts where id IN (355);
select u.email, t.crm_id, t.* from teams t
join users u on u.id = t.owner_id
where crm_id IN (97);
SELECT * FROM crm_fields WHERE id = 9...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20904-fix-update-es-on-activity-command, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20904-fix-update-es-on-activity-command","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Events\\Playbooks;\n\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\User;\n\nclass PlaybookCreated\n{\n use SerializesModels;\n\n /**\n * The playbook instance.\n *\n * @var Playbook\n */\n public Playbook $playbook;\n\n /**\n * The user who created the playbook.\n *\n * @var User\n */\n public User $user;\n\n /**\n * Create a new event instance.\n */\n public function __construct(Playbook $playbook, User $user)\n {\n $this->playbook = $playbook;\n $this->user = $user;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Events\\Playbooks;\n\nuse Illuminate\\Queue\\SerializesModels;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\User;\n\nclass PlaybookCreated\n{\n use SerializesModels;\n\n /**\n * The playbook instance.\n *\n * @var Playbook\n */\n public Playbook $playbook;\n\n /**\n * The user who created the playbook.\n *\n * @var User\n */\n public User $user;\n\n /**\n * Create a new event instance.\n */\n public function __construct(Playbook $playbook, User $user)\n {\n $this->playbook = $playbook;\n $this->user = $user;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"30","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"27","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"106","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;","depth":4,"on_screen":true,"value":"SELECT * FROM team_features where team_id = 1;\n\nSELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922\nSELECT * FROM users WHERE team_id = 340; # 12015\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 340\nand sa.provider = 'salesforce';\n# and sa.provider = 'salesloft';\n\nselect * from crm_fields where crm_configuration_id = 270 and object_type = 'event';\n# 125558 - Event Type - Event_Type__c\n# 125552 - Event Status - Event_Status__c\n\nSELECT * FROM sidekick_settings WHERE team_id = 340;\n\nSELECT * FROM crm_field_values WHERE crm_field_id in (125552);\n\nselect * from activities where crm_configuration_id = 270\nand type = 'conference' and crm_provider_id IS NOT NULL\nand actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;\n\nSELECT * FROM activities WHERE id = 20871677;\nSELECT * FROM crm_field_data WHERE activity_id = 20871677;\n\nselect * from crm_layouts where crm_configuration_id = 270;\nselect * from crm_layout_entities where crm_layout_id in (886,887);\n\nSELECT * FROM crm_configurations WHERE id = 270;\n\nselect * from playbooks where team_id = 340; # 1514\nselect * from groups where team_id = 340;\nSELECT * FROM crm_fields WHERE id IN (125393, 125401);\n\nselect g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g\njoin playbooks p on g.playbook_id = p.id\njoin crm_fields f on p.activity_field_id = f.id\nwhere g.team_id = 340;\n\nSELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716\nselect * from crm_field_data where object_id = 20448716;\n\nselect * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008\nselect * from opportunities where team_id = 343;\nselect * from opportunities where team_id = 343 and crm_provider_id = '18099102526';\nselect * from opportunities where team_id = 343 and account_id = 945217482;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from accounts where team_id = 343 order by name asc;\n\nselect * from stages where crm_configuration_id = 273 and type = 'opportunity';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143\nSELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;\nSELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';\nSELECT * FROM activities WHERE id = 20717903;\n\nselect * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 353\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, l.atkinson@mwbsolutions.co.uk\nSELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;\n# id: 20940638, user: 12022, contact: 5305871\nSELECT * FROM activity_summary_logs WHERE activity_id = 20940638;\nselect * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 345\nand sa.provider = 'hubspot';\n\nselect * from users where team_id = 345 and id = 12022;\nSELECT * FROM crm_profiles WHERE user_id = 12022;\nSELECT * FROM participants WHERE activity_id = 20940638;\nSELECT * FROM users u\nJOIN crm_profiles cp ON u.id = cp.user_id\nWHERE u.team_id = 345;\n\nselect * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871\n\nselect * from team_features where team_id = 345;\nSELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197\nSELECT * FROM participants WHERE activity_id = 20897406;\n\n\n\nSELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912\nSELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';\n\n\nSELECT * FROM activities WHERE id = 20946641;\nSELECT * FROM crm_profiles WHERE user_id = 10211;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, triger@lunio.ai\nSELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';\nselect * from stages where crm_configuration_id = 97 and type = 'opportunity';\nselect * from opportunities where team_id = 120;\n\n\nselect * from crm_configurations crm join teams t on crm.id = t.crm_id\nwhere 1=1\nAND t.current_billing_plan IS NOT NULL\nAND crm.auto_sync_activity = 0\nand crm.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,james.lewendon@exclaimer.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 270\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956\nSELECT * FROM crm_profiles WHERE user_id = 11446;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, alex.chikly@cygnetise.com\nselect * from playbooks where team_id = 372;\nselect * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340\nSELECT * FROM crm_field_values WHERE crm_field_id = 141340;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 372\nand sa.provider = 'salesforce';\n\nselect * from crm_profiles where crm_configuration_id = 300;\nSELECT * FROM crm_configurations WHERE team_id = 372;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,mfa@planday.com\nSELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756\nselect * from crm_field_data where object_id = 3207756;\nSELECT * FROM crm_fields WHERE id = 111834;\n\nselect f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value\nFROM crm_fields f\nJOIN crm_field_data fd ON f.id = fd.crm_field_id\nWHERE f.crm_configuration_id = 242\nAND f.object_type = 'opportunity'\nAND fd.object_id IN (3207756)\nORDER BY fd.object_id, fd.updated_at;\n\nSELECT * FROM crm_configurations WHERE auto_connect = 1;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,salesforce-admin@tourlane.com\nselect * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id\nwhere g.team_id = 187;\n\nselect * from `groups` where team_id = 187;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 187\nand sa.provider = 'salesforce';\n\n# Destination - 98870 - Destination__c\n# Stage - 79014 - StageName\n# Land Arrangement - 98856 - Land_Arrangement__c\n# Flight - 98848 - Flight__c\n# Last activity date - 98812 - LastActivityDate\n# Last modified date - 98809 - LastModifiedDate\n# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c\n# next call - 98864 - Next_Call__c\n\nselect * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\nselect * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';\nselect * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;\nselect * from activities where opportunity_id = 3538248;\n\nSELECT * FROM crm_profiles WHERE user_id = 8150;\n\nselect * from deal_risks where opportunity_id = 3538248;\n\nselect * from teams where crm_id IS NULL;\n\nSELECT opp.id AS opportunity_id,\n u.group_id AS group_id,\n MAX(\n CASE\n WHEN a.type IN (\"sms-inbound\", \"sms-outbound\") THEN a.created_at\n ELSE a.actual_end_time\n END) as last_date\nFROM opportunities opp\nleft join activities a on a.opportunity_id = opp.id\ninner join users u on opp.user_id = u.id\nwhere opp.user_id IN (9951)\n\nAND opp.is_closed = 0\nand a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL\ngroup by opp.id;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,polly.morphew@cybsafe.com\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 301;\nSELECT * FROM contacts WHERE id = 6612363;\nSELECT * FROM accounts WHERE id = 4235676;\nSELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;\nselect * from opportunity_stages where opportunity_id = 4503759;\n# SELECT * FROM opportunities WHERE id = 4569937;\n\nselect * from activities where crm_configuration_id = 301;\nSELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370\nSELECT * FROM participants WHERE activity_id = 26330370;\n\nSELECT * FROM teams WHERE id = 375;\nselect * from playbooks where team_id = 375;\n\nselect * from stages where crm_configuration_id = 301 and type = 'opportunity';\n\nselect * from teams;\nselect * from contact_roles;\n\nSELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';\n\nselect * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;\n\nSELECT * FROM crm_field_data WHERE object_id = 3771706;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'\nand crm_provider_id LIKE \"%traffic_light%\";\nSELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);\n\nSELECT fd.* FROM opportunities o\nJOIN crm_field_data fd ON o.id = fd.object_id\nWHERE o.team_id = 343\n# and o.user_id IS NOT NULL\nand fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)\nand fd.value != ''\norder by value desc\n# group by o.id\n;\n\nSELECT * FROM opportunities WHERE id = 3769843;\n\nSELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, salesforce-admin@tourlane.com\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 209;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,aswini.mishra@fundingcircle.com\nSELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839\n\n\nSELECT * FROM opportunities WHERE id = 3855992;\n\nSELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988\n\nSELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';\n\nselect * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507\nSELECT * FROM crm_field_data WHERE object_id = 5874411;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379\nand sa.provider = 'hubspot';\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, nikhil.kumar@mention-me.com\nSELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, salesforce-admin@tourlane.com\nSELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793\nselect * from generic_ai_prompts where subject_id = 3537793;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, triger@lunio.ai\nSELECT * FROM crm_configurations WHERE id = 97;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 97;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;\nSELECT * FROM crm_fields WHERE id = 32682;\n\nselect cfd.value, o.* from opportunities o\njoin crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682\nwhere team_id = 120\nand cfd.value != ''\n;\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 120\nand sa.provider = 'salesforce';\n\nselect * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';\nSELECT * FROM crm_field_data WHERE object_id = 2313439;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 410;\nSELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';\nselect * from scorecards where team_id = 410;\nselect * from scorecard_rules;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, aswini.mishra@fundingcircle.com\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\njoin users u on o.user_id = u.id\nwhere a.crm_configuration_id = 177 and a.type LIKE '%email-out%'\n# and a.actual_end_time > '2024-12-16 00:00:00'\n# and o.remotely_created_at > '2024-12-01 00:00:00'\n# and u.group_id = 1014\nand u.id = 9021\norder by a.id desc;\nSELECT * FROM opportunities WHERE id in (3981384,4017346);\nSELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);\n\nselect * from users where id = 9021;\nselect * from inboxes where user_id = 9021;\n\nselect * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';\n\nselect * from email_messages where team_id = 220\nand orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'\nand subject LIKE '%Personal%'\n# and 'from' = 'credit@fundingcircle.com'\n;\n\nselect * from activities a\njoin opportunities o on a.opportunity_id = o.id\nwhere a.user_id = 9021 and a.type LIKE '%email-out%'\nand a.actual_end_time > '2024-12-18 00:00:00'\nand o.user_id IS NOT NULL\nand o.remotely_created_at > '2024-12-01 00:00:00'\norder by a.id desc;\n\nSELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;\nselect * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;\n\nselect * from team_settings where name IN ('useCloseDate');\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, jfarrell@hurree.co\nSELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 104\nand sa.provider = 'hubspot';\n\nselect * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'\nselect * from teams where crm_id IS NULL;\n\nselect t.name as 'team', u.name as 'owner', u.email, u.phone\nfrom teams t\njoin activity_providers ap on t.id = ap.team_id\njoin users u on t.owner_id = u.id\nwhere 1=1\n and t.status = 'active'\n and ap.is_enabled = 1\n# and u.status = 1\n and ap.provider = 'ms-teams';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nSELECT * FROM teams WHERE id = 442; # 14293\nselect * from users where team_id = 442;\nselect * from social_accounts sa where sa.sociable_id = 14293;\nselect * from invitations where team_id = 442;\n\n# ********************************************************************************************************\nSELECT * FROM users WHERE email LIKE '%nea.liikamaa@eletive.com%'; # 14022\nSELECT * FROM teams WHERE id = 429;\nselect * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);\nselect * from activities where opportunity_id in (4340436,4353519);\n\nselect * from transcription where activity_id IN (25630961,25381771);\nselect * from generic_ai_prompts where subject_id IN (4353519);\n\nSELECT\n a.id as activity_id,\n a.opportunity_id,\n a.type as activity_type,\n a.language,\n CONCAT(a.title, a.description) AS mail_content,\n e.from AS mail_from,\n e.to AS mail_to,\n e.subject AS mail_subject,\n e.body AS mail_body,\n p.type as prompt_type,\n p.status as prompt_status,\n p.content AS prompt_content,\n a.actual_start_time as created_at\nFROM activities a\n LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL\n LEFT JOIN email_messages e ON a.id = e.activity_id\nWHERE a.actual_start_time > '2024-01-01 00:00:00'\n AND a.opportunity_id IN (4353519)\n AND a.status IN ('completed', 'received', 'delivered')\n AND a.deleted_at IS NULL\n AND a.type NOT IN ('sms-inbound', 'sms-outbound')\nORDER BY a.opportunity_id ASC, a.id ASC;\n\nSELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293\nSELECT * FROM teams WHERE id = 442;\nSELECT * FROM crm_configurations WHERE id = 344;\nselect * from team_features where team_id = 442;\nselect * from groups where team_id = 442;\nselect * from playbooks where team_id = 442;\nselect * from playbook_categories where playbook_id = 1729;\nselect * from crm_fields where crm_configuration_id = 344 and id = 172024;\nSELECT * FROM crm_field_values WHERE crm_field_id = 172024;\nselect * from crm_layouts where crm_configuration_id = 344;\nselect * from playbook_layouts where playbook_id = 1729;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444\n\nselect s.*\n# , s.sent_at, u.name, a.*\nfrom activity_summary_logs s\ninner join activities a on a.id = s.activity_id\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 356\nand s.sent_at > date_sub(now(), interval 60 day)\norder by a.actual_end_time desc;\n\nselect * from activities a\n# inner join activity_summary_logs s on s.activity_id = a.id\nwhere a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)\n# and a.crm_provider_id is not null\n# and provider <> 'ringcentral'\nand status = 'completed'\norder by a.actual_end_time desc;\n\nselect * from teams order by id desc; # 17328, 32, 17830, integration-account@jiminny.com\nSELECT * FROM users;\nSELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active\nSELECT * FROM teams WHERE id = 260;\nselect * from team_settings where team_id = 260;\nselect * from crm_configurations where team_id = 260;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 356;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;\n\nselect * from accounts where crm_configuration_id = 221 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 221 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 221 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 221 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 221;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 221 order by id desc;\nselect * from stages where crm_configuration_id = 221 order by id desc;\n\nselect * from accounts where crm_configuration_id = 356 order by id desc; # 7000\nselect * from leads where crm_configuration_id = 356 order by id desc; # 0\nselect * from contacts where crm_configuration_id = 356 order by id desc; # 200 000\nselect * from opportunities where crm_configuration_id = 356 order by id desc; # 0\nselect * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23\nselect * from crm_fields where crm_configuration_id = 356;\nselect * from crm_field_values where crm_field_id = 5302 order by id desc;\nselect * from crm_layouts where crm_configuration_id = 356 order by id desc;\nselect * from stages where crm_configuration_id = 356 order by id desc;\n\nselect * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)\nselect * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)\nselect * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4\nselect ce.* from calendars c\njoin users u on c.user_id = u.id\njoin calendar_events ce on c.id = ce.calendar_id\nwhere u.team_id = 260\nand (ce.start_time > '2025-02-21 00:00:00')\n;\n# calendar events 1207\n#\n\nselect * from opportunities where team_id = 260;\nSELECT * FROM crm_field_data WHERE object_id = 4696496;\n\nselect * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;\nselect * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')\n# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0\nand created_at > '2024-03-01 00:00:00'\norder by id desc; # 880 000, ringcentral, avaya\nSELECT * FROM participants WHERE activity_id = 26371744;\n\n# all activities 942 000 +\n# conference 7385 - scheduled 984 - external 343\n\nselect * from activities where id = 26321812;\nselect * from participants where activity_id = 26321812;\nselect * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);\nselect * from leads where id in (720428,689175,731546,645866,621037);\n\nselect * from users where id = 13841;\nselect * from opportunities where user_id = 9541;\nselect * from stages where id = 15900;\n\nselect * from accounts where\n# id IN (4160055,5053725,4965303,4896434)\nid in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)\n;\n\nselect * from activities where id = 26654935;\nSELECT * FROM opportunities WHERE id = 4803458;\n\nSELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;\nSELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time\nFROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);\n\nSELECT DISTINCT\n o.id, o.stage_id, s.name, a.title,\n a.*\nFROM activities a\n# INNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nINNER JOIN groups g ON u.group_id = g.id\nINNER JOIN opportunities o ON a.opportunity_id = o.id\nINNER JOIN stages s ON o.stage_id = s.id\nWHERE\n a.crm_configuration_id = 356\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 13841\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')\n AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')\n\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')\n )\n )\n AND (\n# s.id = 15900\n s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')\n OR s.uuid IS NULL -- Include records without opportunity stage\n )\n\nORDER BY a.actual_end_time DESC;\n# ********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, willsc@leadforensics.com\nSELECT * FROM users WHERE team_id = 190;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 190\nand sa.provider = 'hubspot';\n\nselect * from role_user where user_id = 8474;\n\nselect * from crm_configurations where provider = 'bullhorn';\n\nSELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;\nSELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;\n\nSELECT * FROM opportunities WHERE id = 4732493;\nselect * from activities where opportunity_id = 4732493;\n\n# ********************************************************************************************************\nSELECT * FROM teams WHERE id = 443; # 358, 14315, andrea.romano@correrenaturale.com\nSELECT * FROM opportunities WHERE team_id = 443;\n\nSELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id\nFROM activities AS a\nJOIN stages AS s ON a.stage_id = s.id\nJOIN users AS u ON u.id = a.user_id\nJOIN teams AS t ON t.id = s.team_id\nWHERE u.team_id <> s.team_id and t.id > 135;\n\n\nSELECT\n crm_configuration_id,\n crm_provider_id,\n COUNT(*) as duplicate_count,\n GROUP_CONCAT(id) as stage_ids,\n GROUP_CONCAT(name) as stage_names\nFROM stages\nGROUP BY crm_configuration_id, crm_provider_id\nHAVING COUNT(*) > 1\nORDER BY duplicate_count DESC;\n\nselect * from stages where id IN (14898,14907);\n\nselect * from business_processes;\n\nSELECT *\nFROM crm_configurations\nWHERE team_id IN (\n SELECT team_id\n FROM crm_configurations\n GROUP BY team_id\n HAVING COUNT(*) > 1\n)\nORDER BY team_id;\n\nSELECT *\nFROM teams\nWHERE crm_id IN (\n SELECT crm_id\n FROM teams\n GROUP BY crm_id\n HAVING COUNT(*) > 1\n)\nORDER BY crm_id;\n\n# ***************************************************************************\nselect * from crm_configurations where provider = 'integration-app';\nSELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 andrea.romano@correrenaturale.com\nselect * from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;\nselect * from team_features where team_id = 358;\nselect * from activity_summary_logs;\n\nselect * from teams where id = 406;\n\n# ************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, srv.salesforce@sportfive.com\nselect * from activities where crm_configuration_id = 202 order by actual_end_time desc;\n\nSELECT * FROM users where id = 14637;\nSELECT * FROM teams where id = 267;\nSELECT * FROM groups where id = 1118;\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 202\n AND status IN ('completed', 'failed')\n AND recording_state != 'stopped'\n AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n AND (is_private = 0 OR user_id = 14637)\n AND (\n (\n actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n ) OR (\n actual_start_time IS NULL\n AND type IN ('sms-outbound', 'sms-inbound')\n AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND NOT EXISTS (\n SELECT 1\n FROM tracks\n WHERE\n tracks.activity_id = activities.id\n AND tracks.type IN ('audio', 'video')\n )\nORDER BY actual_end_time DESC;\n\nSELECT DISTINCT\n a.*\nFROM activities a\nINNER JOIN tracks t ON a.id = t.activity_id\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams team ON u.team_id = team.id\nWHERE\n a.crm_configuration_id = 202\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n# and a.user_id = 14637\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND t.type IN ('audio', 'video')\n AND (\n (a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')\n OR\n (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'\n )\n )\n AND (\n a.is_private = 0\n OR (\n a.is_private = 1\n AND a.user_id = 14637\n )\n )\n\nORDER BY a.actual_end_time DESC\n;\n\nSELECT DISTINCT a.*\nFROM activities a\nINNER JOIN users u ON a.user_id = u.id\nINNER JOIN teams t ON u.team_id = t.id\n# INNER JOIN tracks tr ON a.id = tr.activity_id\n# INNER JOIN groups g ON u.group_id = g.id\nWHERE 1=1\n AND t.id = 267\n# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')\n AND a.status IN ('completed', 'failed')\n AND a.recording_state != 'stopped'\n AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n# AND tr.type NOT IN ('audio', 'video')\n AND (\n a.is_private = 0\n OR a.user_id = 14637\n )\n AND (\n (a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')\n OR (\n a.actual_start_time IS NULL\n AND a.type IN ('sms-outbound', 'sms-inbound')\n AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'\n )\n )\n# and NOT EXISTS (\n# SELECT 1\n# FROM tracks t\n# WHERE t.activity_id = a.id\n# AND t.type IN ('audio', 'video')\n# )\n\nORDER BY a.actual_end_time DESC;\n\nSELECT * FROM tracks WHERE activity_id = 26485995;\n\nselect a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\nwhere a.crm_configuration_id = 202\n# and a.is_internal = 0\nand (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type IN (\"softphone\",\"softphone-inbound\",\"conference\",\"sms-inbound\")\nand a.status IN ('completed', 'failed')\n# and a.external_id is not null\norder by a.actual_end_time desc;\n\nselect * from activities a where a.crm_configuration_id = 202\nand a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'\n# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')\n\nselect g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a\ninner join users u on u.id = a.user_id\ninner join groups g on g.id = u.group_id\nwhere a.crm_configuration_id = 202\nand a.is_internal = 0\nand (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')\nand a.type = 'conference'\nand a.status != 'completed'\nand a.external_id is not null\norder by a.scheduled_start_time desc;\n\nSELECT * FROM teams WHERE name LIKE '%Tourlane%';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';\nSELECT * FROM crm_field_data WHERE crm_field_id = 98809;\n\nselect * from users where status = 1 AND timezone = 'MDT';\n\nselect * from opportunities where id = 3769814;\nselect * from deal_risks where opportunity_id = 3769814;\n\nselect cp.* from crm_profiles cp\njoin users u on cp.user_id = u.id\njoin crm_configurations crm on cp.crm_configuration_id = crm.id\nwhere crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';\n\nselect * from crm_fields where id = 154575;\n\nselect * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';\nSELECT * FROM teams WHERE id = 176; # crm 148\nselect * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nselect * from crm_fields cf\njoin crm_configurations crm on crm.id = cf.crm_configuration_id\nwhere crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');\n\n# *********************************************************************************************\nSELECT * FROM users WHERE id IN (15415, 15418);\nSELECT * FROM groups WHERE id IN (1805,1806);\nSELECT * FROM playbooks WHERE id = 1860;\nSELECT * FROM playbook_categories WHERE id = 38634;\nSELECT * FROM crm_fields WHERE id = 189962;\n\nSELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 raza.gilani@vuelio.com\n\nSELECT * FROM crm_profiles WHERE user_id = 15415;\nSELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';\n\nselect * from sidekick_settings where team_id = 472;\n\nSELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418\nSELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415\nSELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415\n\n# *********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, salesforce-integrations@teamtailor.com\nselect * from crm_configurations where id = 218;\nSELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765\nSELECT * FROM users WHERE id IN (13232, 13230);\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n0057R00000EPL5HQAX Inez Ekblad\n\n1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur\n\nSELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);\n\n############################################################################################\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id IN (94491,94493,94498);\nSELECT * FROM users WHERE id = 13658;\nSELECT * FROM teams WHERE id = 109;\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, katy.holden@strengthscope.comk\nSELECT * FROM stages WHERE crm_configuration_id = 390;\nselect * from business_processes where team_id = 481 and crm_configuration_id = 390;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 481\nand sa.provider = 'salesforce';\n\n\nSELECT * FROM users WHERE id = 15780; # team 462\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 462\nand sa.provider = 'hubspot';\n\n\nselect * from teams where id = 495;\nSELECT * FROM users WHERE id = 15794;\nselect * from social_accounts where sociable_id = 15794;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752\nSELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794\nSELECT * FROM activities WHERE crm_configuration_id = 407\nand status = 'completed' and type = 'conference'\norder by id desc;\n\nselect ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id\njoin permission_role pr on pr.role_id = ru.role_id\n join permissions p on p.id = pr.permission_id\nwhere team_id = 495 and p.name IN ('dial');\n\nselect * from permission_role;\n\nselect * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;\nSELECT * FROM activities WHERE id = 29512773;\nSELECT * FROM activities WHERE id IN (29042721,28991325,29002874);\n\nSELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 407\n# and a.id IN (29042721,28991325,29002874);\n\nSELECT * FROM users WHERE id = 15794;\nSELECT * FROM users WHERE team_id = 495;\nSELECT * FROM social_accounts WHERE sociable_id = 15794;\nSELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';\nSELECT * FROM contacts WHERE team_id = 495;\nSELECT * FROM leads WHERE team_id = 495;\nSELECT * FROM accounts WHERE team_id = 495;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 407;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 407;\nSELECT * FROM crm_configurations WHERE id = 407;\nSELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'\nand user_id IS NOT NULL and is_closed = 1 and is_won = 1;\n\n# ********************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103\nSELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064\nSELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');\n\n# *********************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 325\nand sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085\nSELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733\nSELECT * FROM activity_summary_logs where activity_id = 28719733;\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444\nSELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';\nSELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630\nselect * from activities where crm_configuration_id = 356 and lead_id = 841732;\n\nSELECT * from activity_summary_logs al join activities a on a.id = al.activity_id\nwhere a.crm_configuration_id = 356;\n\nselect * from activities where crm_configuration_id = 356\nand actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'\norder by id desc;\n\nselect * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;\nselect * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\nselect * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;\n\nselect * from team_features where team_id = 260;\nselect * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);\n\nSELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;\n\nselect * from crm_fields;\nselect * from crm_layout_entities;\n\nSELECT * FROM teams WHERE name LIKE '%Optable%';\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969\nSELECT * FROM crm_configurations WHERE id = 218;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 109\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939\nSELECT * FROM crm_field_data WHERE activity_id = 28655939;\nSELECT * FROM crm_fields WHERE id in (94491,94493,94498);\n\nselect * from teams where crm_id IS NULL;\n\nSELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;\n\n# *************************************************************************************************\nselect * from team_domains where team_id = 399;\nSELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207\n\nselect * from calendar_events where id = 5163781;\nSELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896\nSELECT * FROM participants WHERE activity_id = 29443896;\nselect * from contacts where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\nselect * from leads where crm_configuration_id = 318 and email = 'marianne.westeng@strawberry.no';\n\nselect * from activities where user_id = 14937 order by created_at ;\n\nselect * from users where id = 14937;\n\nselect * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';\nselect * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';\n\nselect * from activities a join participants p on a.id = p.activity_id\nwhere crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';\n\n# *************************************************************************************************\nSELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';\nSELECT * FROM opportunities WHERE team_id = 379 order by id desc;\nSELECT * FROM teams WHERE id = 379;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 379 and sociable_id = 13852\nand sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE id = 307;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 307;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 307\n and id IN (144750,144855,145158,155227);\n\nSELECT * FROM activities;\n\n\nselect * from activities\nwhere created_at > '2025-07-01 00:00:00'\n# and created_at < '2025-08-01 00:00:00'\nand type not in ('email-outbound', 'email-inbound')\nand account_id is null\nand contact_id is null\nand lead_id is null\nand opportunity_id is not null\n;\nSELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);\nSELECT * FROM crm_configurations WHERE id in (335,301,200);\n\nselect * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';\n\nSELECT * FROM teams WHERE name LIKE '%Resights%';\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\n\nselect * from crm_configurations where provider = 'bullhorn'; # 344\nselect * from teams where id IN (442);\n\nselect * from activities\nwhere crm_configuration_id = 177\nand provider = 'amazon-connect'\n order by id desc;\n# and source <> 'gong';\n\nselect * from activity_providers where provider = 'amazon-connect';\n\nSELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;\n\n\nselect * from crm_configurations where store_transcript = 1;\nSELECT * FROM teams WHERE id IN (80);\n\n# *************************************************************************************************\nSELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 277\nand sa.provider = 'salesforce';\n\nselect * from activities where crm_configuration_id = 213 and account_id = 2511502;\n\nselect * from crm_configurations where id = 213;\n\nSELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604\nSELECT * FROM participants WHERE activity_id = 33981604;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';\n\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 431\nand sa.provider = 'salesforce';\nSELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223\nselect * from activity_summary_logs where activity_id = 33997223;\nselect * from activity_notes where activity_id = 33997223;\n\n# ***********************************\nSELECT * FROM teams WHERE name LIKE '%Abode%';\n\n\nselect * from features;\nselect * from teams t\nwhere t.status = 'active'\nand id NOT IN (select team_id from team_features where feature_id = 9)\n;\n\n\nselect * from playbook_layouts where playbook_id = 1725;\nSELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473\nselect * from teams where id = 318;\nselect * from crm_configurations where team_id = 318;\nselect * from playbooks where team_id = 318;\nSELECT * FROM crm_layouts where crm_configuration_id = 381;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;\nSELECT * FROM crm_fields WHERE id IN (192938,192936,192939);\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;\nSELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);\n\nSELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533\n\n\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nSELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);\n\nSELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124\nselect * from crm_layouts where crm_configuration_id = 281;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;\nselect * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);\nselect * from opportunities where crm_configuration_id = 281;\n\nSELECT * FROM activities WHERE id IN (34211315, 34130075);\nSELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);\n\nselect cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd\njoin crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id\njoin crm_fields cf on cle.crm_field_id = cf.id\nwhere cf.deleted_at IS NOT NULL\nGROUP BY cle.id, cf.id;\n\nselect * from crm_layouts where id IN (355);\nselect u.email, t.crm_id, t.* from teams t\njoin users u on u.id = t.owner_id\nwhere crm_id IN (97);\n\nSELECT * FROM crm_fields WHERE id = 96492;\n\nselect * from permissions;\nselect * from permission_role where permission_id = 247;\nselect * from roles;\n\nselect * from migrations;\n# *****************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('291e3c21-11cc-4728-aee7-6e4bedf86d72') = uuid; # 34262174\nSELECT * FROM crm_configurations WHERE id = 301;\nSELECT * FROM teams WHERE id = 343;\nselect * from social_accounts sa\njoin users u on sa.sociable_id = u.id\nwhere u.team_id = 343\nand sa.provider = 'hubspot';\n\nselect * from participants where activity_id = 34262174;\n\nselect * from contacts where crm_configuration_id = 301 and id = 6976326;\nselect * from accounts where crm_configuration_id = 301 and id IN (4647626, 4815829); # 30761335403\n\nselect * from activity_summary_logs where activity_id = 34262174;\n\nselect * from users where status = 1 AND timezone = 'EST';\n\n# ****************************************************************************\nSELECT * FROM users WHERE id = 13869;\nSELECT * FROM crm_configurations WHERE id = 320;\nSELECT * FROM teams WHERE id = 401;\n\nSELECT * FROM activities WHERE uuid_to_bin('2228c16f-10be-48d5-90d4-67385219dc01') = uuid; # 29670601\n\nSELECT * FROM accounts WHERE id = 7761483;\nSELECT * FROM opportunities WHERE id = 6051814;\n\nSELECT * FROM teams WHERE name LIKE '%Seedlegals%';\n\n;select * from opportunities where updated_at > '2025-10-11' AND crm_provider_id = '34713761166';\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 177;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 577;\nSELECT * FROM crm_fields WHERE id IN (68458,68459,68480,68497,68524,68530,68554,68618,68662,68781,68810,68898,68981,69049,97467);\n\nSELECT t.id, crm.id, t.name, crm.sync_objects, crm.provider, crm.last_synced_at FROM crm_configurations crm join teams t on t.crm_id = crm.id\nwhere t.status = 'active' AND crm.provider = 'hubspot' AND crm.last_synced_at < '2025-10-22 00:00:00';\n\nSELECT * FROM activities WHERE uuid_to_bin('fa09449f-cba9-496a-b8f3-865cd3c72351') = uuid;\nSELECT * FROM crm_configurations where id = 184;\nSELECT * FROM teams WHERE id = 246;\nSELECT * FROM social_accounts WHERE sociable_id = 9259 and provider = 'hubspot';\n\nSELECT * FROM users WHERE email LIKE '%rhian.old@bud.co.uk%'; # 17700\nSELECT * FROM teams WHERE id = 551;\n\nSELECT * FROM crm_configurations WHERE id = 471;\nSELECT * FROM activities WHERE crm_configuration_id = 471 and crm_provider_id IS NOT NULL;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 471;\nSELECT * FROM crm_fields WHERE id = 307260;\nSELECT * FROM crm_field_values WHERE crm_field_id = 307260;\n\nselect * from crm_layouts where crm_configuration_id = 471;\n\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1547;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1548;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 551 and sa.provider = 'hubspot';\n\nSELECT * FROM teams WHERE name LIKE '%$PCS%';\n\n# ********************************************************************************************************\nselect * from crm_configurations crm\njoin teams t on t.crm_id = crm.id\nwhere t.status = 'active'\nand crm.provider = 'hubspot';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from crm_configurations where id = 331; # 416\nSELECT * FROM teams WHERE id = 416;\nSELECT * FROM opportunities WHERE team_id = 190;\n\nSELECT * FROM teams WHERE name LIKE '%Lead Forensics%';\nSELECT sa.id,\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 190 and sa.provider = 'hubspot';\n\n\n\nSELECT * FROM teams WHERE name LIKE '%Rapaport%'; # 431, 337\nSELECT * FROM teams where id = 431;\nSELECT * FROM crm_configurations where team_id = 431;\nSELECT * FROM activity_providers where team_id = 431;\nSELECT * FROM activities where crm_configuration_id = 337 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 431 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%BiP%'; # 401, 320\nSELECT * FROM teams where id = 401;\nSELECT * FROM crm_configurations where team_id = 401;\nSELECT * FROM activity_providers where team_id = 401;\nSELECT * FROM activities where crm_configuration_id = 320 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\nSELECT sa.id,\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 401 and sa.provider = 'salesforce';\n\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 307; # 379 - Story Terrace Inc , portalId: 3921157\nSELECT * FROM contacts WHERE team_id = 379 and updated_at > '2026-01-31 11:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 379 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 379 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; # 563 - LATUS Group (ad94d501-5d09-44fd-878f-ca3a9f8865c3) , portalId: 3904501\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 563 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 338; # 432 - Formalize , portalId: 9214205\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 432 and sa.provider = 'hubspot';\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 432 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 436; # 519 - Moxso , portalId: 25531989\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 519 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 96; # 119 - Nourish Care , portalId: 26617984\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-02 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 119 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 331; # 416 - The National College , portalId: 7213852\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 416 and updated_at > '2026-02-04 11:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 308; # 380 - Foodles , portalId: 7723616\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 380 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 379; # 471 - imat-uve , portalId: 9177354\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 471 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 465; # 545 - Spotler , portalId: 144759271\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 545 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 455; # 537 - indevis , portalId: 25666868\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 537 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 200; # 265 - Jobadder , portalId: 6426676\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 265 and updated_at > '2026-02-06 10:30:00' order by updated_at desc;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 335; # 429 - Eletive , portalId: 6110563\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 429 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 363; # 456 - Global Group , portalId: 8901981\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 456 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 297; # 369 - Unbiased , portalId: 9229005\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 369 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 353; # 449 - Fuuse , portalId: 25781745\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 449 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 487; # 566 - Nimbus , portalId: 39982590\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 566 and updated_at > '2026-02-09 10:30:00' order by updated_at desc;\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 487;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1630;\nselect * from crm_fields where crm_configuration_id = 487 and\n(uuid_to_bin('4c6b2971-64d4-45b8-b377-427be758b5a5') = uuid or uuid_to_bin('59e368d8-65a0-4b77-b611-db37c99fbe68') = uuid);\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 420; # 506 - voiio , portalId: 145629154\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 506 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 479; # 558 - Momice , portalId: 535962\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 558 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 59; # 80 - Storyclash GmbH , portalId: 4268479\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 80 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 175; # 203 - Team iAM , portalId: 5534732\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 203 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 368; # 460 - OneTouch Health , portalId: 5534732183355\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 460 and updated_at > '2026-02-10 15:00:00' order by updated_at desc;\n\n\n\nselect * from users where id = 29643;\nSELECT * FROM crm_field_values WHERE crm_field_id = 375177;\n# ********************************************************************\nSELECT * FROM teams WHERE name LIKE '%Buynomics%'; # 462, 482, 14910\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\n# and description like '%The call focused on understanding Welch%'\norder by id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 462 and sa.provider = 'salesforce';\n\nselect * from contacts where crm_configuration_id = 482 and name = 'Cyndall Hill'; # 15504749\nselect * from contacts where id = 10891096; # 482\nSELECT * FROM activities WHERE crm_configuration_id = 482\nand type NOT IN ('email-inbound', 'email-outbound')\nand contact_id = 15504749\norder by id desc;\n\nselect * from activities where id = 36793003; # 96cc7bc1-8622-4d27-92f4-baf664fc1a56, 00UOf00000PDdOXMA1\nselect * from transcription where id = 7646782;\nselect * from ai_prompts where transcription_id = 7646782;\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7a8471a3-847e-4822-802b-ddf426bbc252') = uuid; # 37370018\nSELECT * FROM activity_summary_logs WHERE activity_id = 37370018;\nSELECT * FROM teams WHERE id = 555;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 555 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('7c17b8aa-09df-4f85-a0f7-51f47afd712d') = uuid; # 37395250\nSELECT * FROM activities WHERE uuid_to_bin('14d60388-260d-494b-aa0d-63fdb1c78026') = uuid; # 37395250\n\nSELECT a.* FROM activities a JOIN crm_configurations c on c.id = a.crm_configuration_id\nwhere a.type IN ('softphone', 'softphone-outbound') and c.provider = 'hubspot'\nand a.provider NOT IN ('hubspot')\n# and a.provider IN ('salesloft')\n# and c.id NOT IN (70)\n# and a.duration > 30\n# and actual_start_time > '2026-02-05 00:00:00'\norder by a.id desc;\n\nSELECT * FROM activities WHERE id = 37549787;\nSELECT * FROM crm_profiles WHERE user_id = 17613;\n\nSELECT * FROM crm_configurations WHERE id = 70;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 93 and sa.provider = 'hubspot';\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations WHERE id = 373; # KPSBremen.de 465 # - no social account\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 465 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 494;\n\nSELECT * FROM teams WHERE name LIKE '%splose%'; # 572, 495, 18708\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 572 and sa.provider = 'pipedrive';\n\nselect * from opportunities where team_id = 572\n# and name like '%Onebright%'\n# and is_closed = 1 and is_won = 0\n order by id desc;\n\n\nselect * from users where deleted_at is null and status = 2;\n\nselect * from contacts where id = 17900517;\nselect * from accounts where id = 10109838;\nselect * from opportunities where id = 6955880;\n\nselect * from opportunity_contacts where opportunity_id = 6955880;\nselect * from opportunity_contacts where contact_id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nSELECT * FROM activities WHERE uuid_to_bin('adcb8331-5988-4353-834e-383a355abba2') = uuid; # 38056424, crm 104659682404\nselect * from teams where id = 456;\nSELECT * FROM crm_configurations WHERE id = 363;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 456 and sa.provider = 'hubspot';\n\nselect * from crm_layouts where crm_configuration_id = 363;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (1203, 1204, 1635);\nSELECT * FROM crm_fields WHERE id IN (181536, 181538, 213455);\n\nSELECT * FROM teams WHERE name LIKE '%Electric%'; # 342, 272, 12767\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and name like 'NORTHUMBRIA POL%'; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 order by remotely_created_at asc; # and updated_at > '2025-07-01 00:00:00';\nSELECT * FROM opportunities WHERE crm_configuration_id = 272 and updated_at > '2026-01-01 00:00:00';\nSELECT * FROM crm_fields WHERE crm_configuration_id = 272 and object_type = 'opportunity';\nSELECT * FROM crm_field_values WHERE crm_field_id = 127164;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 342 and sa.provider = 'pipedrive';\n\nSELECT * FROM teams WHERE id = 472;\nSELECT * FROM crm_configurations WHERE id = 380;\nselect * from activities where id = 38285673; # 38285673\nSELECT * FROM users WHERE id = 16942;\nSELECT * FROM groups WHERE id = 1964;\nSELECT * FROM playbooks WHERE id = 2033;\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 499; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1678;\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\n\nSELECT * FROM activities WHERE uuid_to_bin('96b1261f-2357-49f9-ab38-23ce12008ea0') = uuid;\n\nselect * from contacts c\nwhere c.crm_configuration_id = 370 order by c.updated_at desc;\n\nSELECT * FROM participants where activity_id = 38833541;\nSELECT * FROM participants where activity_id = 39216301;\nSELECT * FROM activity_summary_logs where activity_id = 39216301;\nSELECT * FROM activities WHERE uuid_to_bin('c7d99fbe-1fb1-41f2-8f4d-52e2bf70e1e9') = uuid; # 38833541, crm 478116564181\nSELECT * FROM activities WHERE uuid_to_bin('2e6ff4d3-9faa-447a-a8c1-9acde4d885ae') = uuid; # 39216301, crm 480171536586\nselect * from crm_profiles where crm_configuration_id = 319 and crm_provider_id = 525785080;\nselect * from opportunities where crm_configuration_id = 319 and crm_provider_id = 410150124747;\nselect * from accounts where crm_configuration_id = 319 and crm_provider_id = 47150650569;\nselect * from contacts where crm_configuration_id = 319 and crm_provider_id IN ('665587441856', '742723347700');\n# owner 13236 525785080\n# contact 1 16779180 665587441856 - activity - Alex Howes alex@supportroom.com created 2026-01-26\n# contact 2 19247563 742723347700 - ash@supportroom.com 2026-03-24\n# company 4176133 47150650569\n# deal 7100953 410150124747\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 400 and sa.provider = 'hubspot';\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556; # owner: 18101, crm: 477\nselect * from crm_configurations where id = 477;\nSELECT * FROM users WHERE id = 18101;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'integration-app';\n\nselect * from opportunities where id = 7594349;\nselect * from opportunity_stages where opportunity_id = 7594349 order by created_at desc;\nselect * from business_processes where id = 6024;\nselect * from business_process_stages where stage_id = 16352;\nselect * from business_process_stages where business_process_id = 6024;\nselect * from stages where team_id = 459;\nselect * from teams where id = 459;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 459 and sa.provider = 'hubspot';\n\nSELECT os.stage_id, s.crm_provider_id, s.name, COUNT(*) as cnt\nFROM opportunity_stages os\nJOIN stages s ON s.id = os.stage_id\nWHERE os.opportunity_id = 7594349\nGROUP BY os.stage_id, s.crm_provider_id, s.name\nORDER BY cnt DESC;\n\nSELECT s.id, s.crm_provider_id, s.name, s.team_id, s.crm_configuration_id\nFROM stages s\nJOIN business_process_stages bps ON bps.stage_id = s.id\nWHERE bps.business_process_id = 6024\nAND s.crm_provider_id = 'contractsent';\n\nselect * from stages where id IN (16352,20612,18281,7344,16378,16309,5036,15223,14535,6293,12098,11607)\n\nSELECT * FROM teams WHERE name LIKE '%Pulsar Group%'; # 472, 380, 15138, raza.gilani@vuelio.com\nselect * from playbooks where team_id = 472; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 2288;\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 380;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 472 and sa.provider = 'salesforce';\n\nselect * from activities where id = 58081273;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2;\n\nSELECT * FROM users WHERE name LIKE '%Neil Hoyle%'; # 17651\nSELECT * FROM social_accounts WHERE sociable_id = 17651;\n\nSELECT * FROM activities WHERE uuid_to_bin('975c6830-7d49-4c1e-b2e9-ac80c10a738a') = uuid;\nSELECT * FROM opportunities WHERE id IN (7842553, 6211727);\nSELECT * FROM contacts WHERE id IN (10202724, 6211727);\nSELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 519 and sa.provider = 'hubspot';\n\nselect * from crm_configurations where id = 436;\nselect * from crm_profiles where crm_configuration_id = 436; # 76091797 -> 16612\n\nselect * from contact_roles where contact_id = 10202724;\n\nselect * from stages where team_id = 519; # 18778\n18775\n\nSELECT\n id,\n crm_provider_id,\n stage_id,\n is_closed,\n is_won,\n stage_updated_at,\n updated_at\nFROM opportunities\nWHERE id IN (6211727, 7842553);\n\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id = 6211727 AND contact_id = 10202724;\n\nSELECT id, name, stage_id, is_closed, is_won, updated_at, remotely_created_at\nFROM opportunities\nWHERE account_id = 8179134\nORDER BY updated_at DESC;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Security","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityActualDate.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityChannel.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityDurationRange.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityPlaylistIn.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityProviderIn.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityRecorded.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityRecordingStopped.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityScheduledDate.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityStatusIn.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityType.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityUpdatedDate.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoreFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutoScoreFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ClosedDealsFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedbackAverageScore.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedbackCoachUserIn.php, final class","depth":11,"on_screen":false,"role_description":"text"}]...
|
4973879050592344837
|
2074680717582186093
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20904-fix-update-es-on Project: faVsco.js, menu
JY-20904-fix-update-es-on-activity-command, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Events\Playbooks;
use Illuminate\Queue\SerializesModels;
use Jiminny\Models\Playbook;
use Jiminny\Models\User;
class PlaybookCreated
{
use SerializesModels;
/**
* The playbook instance.
*
* @var Playbook
*/
public Playbook $playbook;
/**
* The user who created the playbook.
*
* @var User
*/
public User $user;
/**
* Create a new event instance.
*/
public function __construct(Playbook $playbook, User $user)
{
$this->playbook = $playbook;
$this->user = $user;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
30
9
27
3
106
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM team_features where team_id = 1;
SELECT * FROM teams WHERE name LIKE '%Vixio%'; # 340,270,11922
SELECT * FROM users WHERE team_id = 340; # 12015
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 340
and sa.provider = 'salesforce';
# and sa.provider = 'salesloft';
select * from crm_fields where crm_configuration_id = 270 and object_type = 'event';
# 125558 - Event Type - Event_Type__c
# 125552 - Event Status - Event_Status__c
SELECT * FROM sidekick_settings WHERE team_id = 340;
SELECT * FROM crm_field_values WHERE crm_field_id in (125552);
select * from activities where crm_configuration_id = 270
and type = 'conference' and crm_provider_id IS NOT NULL
and actual_start_time > '2024-09-16 09:00:00' order by scheduled_start_time;
SELECT * FROM activities WHERE id = 20871677;
SELECT * FROM crm_field_data WHERE activity_id = 20871677;
select * from crm_layouts where crm_configuration_id = 270;
select * from crm_layout_entities where crm_layout_id in (886,887);
SELECT * FROM crm_configurations WHERE id = 270;
select * from playbooks where team_id = 340; # 1514
select * from groups where team_id = 340;
SELECT * FROM crm_fields WHERE id IN (125393, 125401);
select g.name as 'team name', p.name as 'playbook name', f.label as 'activity type field' from groups g
join playbooks p on g.playbook_id = p.id
join crm_fields f on p.activity_field_id = f.id
where g.team_id = 340;
SELECT * FROM activities WHERE uuid_to_bin('0c180357-67d2-419e-a8c3-b832a3490770') = uuid; # 20448716
select * from crm_field_data where object_id = 20448716;
select * from activities where crm_configuration_id = 270 and provider = 'salesloft' order by id desc;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%CybSafe%'; # 343,273,12008
select * from opportunities where team_id = 343;
select * from opportunities where team_id = 343 and crm_provider_id = '18099102526';
select * from opportunities where team_id = 343 and account_id = 945217482;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 343
and sa.provider = 'hubspot';
select * from accounts where team_id = 343 order by name asc;
select * from stages where crm_configuration_id = 273 and type = 'opportunity';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Voyado%'; # 353,283,12143
SELECT * FROM activities WHERE crm_configuration_id = 283 and account_id = 3777844 order by id desc;
SELECT * FROM accounts WHERE team_id = 353 AND name LIKE '%Salesloft%';
SELECT * FROM activities WHERE id = 20717903;
select * from participants where activity_id IN (20929172,20928605,20928468,20926272,20926271,20926270,20926269,20916499,20916454,20916436,20916435,20900015,20900014,20900013,20897312,20897243,20897241,20897237,20897232,20897229,20893648,20893231,20893230,20893229,20893228,20889784,20885039,20885038,20885037,20885036,20885035,20882728,20882708,20882703,20882702,20869828,20869811,20869806,20869801,20869799,20869798,20869796,20869795,20869794,20869761,20869760,20869759,20868688,20868687,20850340,20847195,20841710,20833967,20827021,20825307,20825305,20825297,20824615,20824400,20823927,20821760,20795588,20794233,20794057,20793710,20785811,20781789,20781394,20781307,20762651,20758453,20758282,20757323,20756643,20756636,20756629,20756627,20756606,20756605,20756604,20756603,20756602,20756600,20756599,20756598,20756595,20756594,20756589,20756587,20756577,20756573,20748918,20748386,20748385,20748384,20748383,20748382,20748381,20748380,20748379,20748377,20748375,20748373,20743301,20717905,20717904,20717903,20717901,20717899);
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 353
and sa.provider = 'salesforce';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%modern world business solutions%'; # 345,275,12016, [EMAIL]
SELECT * FROM activities WHERE uuid_to_bin('3921d399-3fef-4609-a291-b0097a166d43') = uuid;
# id: 20940638, user: 12022, contact: 5305871
SELECT * FROM activity_summary_logs WHERE activity_id = 20940638;
select * from contacts where team_id = 345 and crm_provider_id = '30891432415' order by name asc; # 5305871
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 345
and sa.provider = 'hubspot';
select * from users where team_id = 345 and id = 12022;
SELECT * FROM crm_profiles WHERE user_id = 12022;
SELECT * FROM participants WHERE activity_id = 20940638;
SELECT * FROM users u
JOIN crm_profiles cp ON u.id = cp.user_id
WHERE u.team_id = 345;
select * from contacts where team_id = 345 and crm_provider_id = '30880813535' order by name desc; # 5305871
select * from team_features where team_id = 345;
SELECT * FROM activities WHERE uuid_to_bin('11701e2d-2f82-4dab-a616-1db4fad238df') = uuid; # 21115197
SELECT * FROM participants WHERE activity_id = 20897406;
SELECT * FROM activities WHERE uuid_to_bin('63ba55cd-1abc-447d-83da-0137000005b7') = uuid; # 20953912
SELECT * FROM activities WHERE crm_configuration_id = 275 and provider = 'ringcentral' and title like '%1252629100%';
SELECT * FROM activities WHERE id = 20946641;
SELECT * FROM crm_profiles WHERE user_id = 10211;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120,97,10984, [EMAIL]
SELECT * FROM opportunities WHERE crm_configuration_id = 97 and crm_provider_id = '006N1000006c5PpIAI';
select * from stages where crm_configuration_id = 97 and type = 'opportunity';
select * from opportunities where team_id = 120;
select * from crm_configurations crm join teams t on crm.id = t.crm_id
where 1=1
AND t.current_billing_plan IS NOT NULL
AND crm.auto_sync_activity = 0
and crm.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Exclaimer%'; # 270,205,10053,[EMAIL]
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 270
and sa.provider = 'salesforce';
SELECT * FROM activities WHERE uuid_to_bin('b54df794-2a9a-4957-8d80-09a600ead5f8') = uuid; # 21637956
SELECT * FROM crm_profiles WHERE user_id = 11446;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Cygnetise%'; # 372,300,12554, [EMAIL]
select * from playbooks where team_id = 372;
select * from crm_fields where crm_configuration_id = 300 and object_type = 'event'; # 141340
SELECT * FROM crm_field_values WHERE crm_field_id = 141340;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 372
and sa.provider = 'salesforce';
select * from crm_profiles where crm_configuration_id = 300;
SELECT * FROM crm_configurations WHERE team_id = 372;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Planday%'; # 291,242,11501,[EMAIL]
SELECT * FROM opportunities WHERE team_id = 291 and crm_provider_id = '006bG000005DO86QAG'; # 3207756
select * from crm_field_data where object_id = 3207756;
SELECT * FROM crm_fields WHERE id = 111834;
select f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value
FROM crm_fields f
JOIN crm_field_data fd ON f.id = fd.crm_field_id
WHERE f.crm_configuration_id = 242
AND f.object_type = 'opportunity'
AND fd.object_id IN (3207756)
ORDER BY fd.object_id, fd.updated_at;
SELECT * FROM crm_configurations WHERE auto_connect = 1;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150,[EMAIL]
select * from group_deal_risk_types drgt join groups g on drgt.group_id = g.id
where g.team_id = 187;
select * from `groups` where team_id = 187;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 187
and sa.provider = 'salesforce';
# Destination - 98870 - Destination__c
# Stage - 79014 - StageName
# Land Arrangement - 98856 - Land_Arrangement__c
# Flight - 98848 - Flight__c
# Last activity date - 98812 - LastActivityDate
# Last modified date - 98809 - LastModifiedDate
# Last inbound mail timestamp - 99151 - Last_Inbound_Mail_Timestamp__c
# next call - 98864 - Next_Call__c
select * from crm_fields where crm_configuration_id = 209 and object_type = 'opportunity';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 209;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;
select * from opportunities where team_id = 187 and name LIKE'%Muriel Sal%';
select * from opportunities where team_id = 187 and user_id = 9951 and is_closed = 0;
select * from activities where opportunity_id = 3538248;
SELECT * FROM crm_profiles WHERE user_id = 8150;
select * from deal_risks where opportunity_id = 3538248;
select * from teams where crm_id IS NULL;
SELECT opp.id AS opportunity_id,
u.group_id AS group_id,
MAX(
CASE
WHEN a.type IN ("sms-inbound", "sms-outbound") THEN a.created_at
ELSE a.actual_end_time
END) as last_date
FROM opportunities opp
left join activities a on a.opportunity_id = opp.id
inner join users u on opp.user_id = u.id
where opp.user_id IN (9951)
AND opp.is_closed = 0
and a.status IN ('completed', 'received', 'delivered') OR a.status IS NULL
group by opp.id;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Cybsafe%'; # 343,301,12008,[EMAIL]
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 343
and sa.provider = 'hubspot';
SELECT * FROM crm_profiles WHERE crm_configuration_id = 301;
SELECT * FROM contacts WHERE id = 6612363;
SELECT * FROM accounts WHERE id = 4235676;
SELECT * FROM opportunities WHERE crm_configuration_id = 301 and crm_provider_id = 32983784868;
select * from opportunity_stages where opportunity_id = 4503759;
# SELECT * FROM opportunities WHERE id = 4569937;
select * from activities where crm_configuration_id = 301;
SELECT * FROM activities WHERE uuid_to_bin('d3b2b28b-c3d0-4c2d-8ed0-eef42855278a') = uuid; # 26330370
SELECT * FROM participants WHERE activity_id = 26330370;
SELECT * FROM teams WHERE id = 375;
select * from playbooks where team_id = 375;
select * from stages where crm_configuration_id = 301 and type = 'opportunity';
select * from teams;
select * from contact_roles;
SELECT * FROM opportunities WHERE team_id = 343 and user_id = 12871 and close_date >= '2024-11-01';
select * from users u join crm_profiles cp on cp.user_id = u.id where u.team_id = 343;
SELECT * FROM crm_field_data WHERE object_id = 3771706;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 343
and sa.provider = 'hubspot';
SELECT * FROM crm_fields WHERE crm_configuration_id = 301 and object_type = 'opportunity'
and crm_provider_id LIKE "%traffic_light%";
SELECT * FROM crm_field_values WHERE crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531);
SELECT fd.* FROM opportunities o
JOIN crm_field_data fd ON o.id = fd.object_id
WHERE o.team_id = 343
# and o.user_id IS NOT NULL
and fd.crm_field_id IN (144020,144048,144111,144113,144126,144481,144508,144531)
and fd.value != ''
order by value desc
# group by o.id
;
SELECT * FROM opportunities WHERE id = 3769843;
SELECT * FROM teams WHERE name LIKE '%Tour%'; # 187,209,8150, [EMAIL]
SELECT * FROM crm_layouts WHERE crm_configuration_id = 209;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 682;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Funding Circle%'; # 220,177,8603,[EMAIL]
SELECT * FROM activities WHERE uuid_to_bin('7a40e99b-3b37-4bb1-b983-325b81801c01') = uuid; # 23139839
SELECT * FROM opportunities WHERE id = 3855992;
SELECT * FROM users WHERE name LIKE '%Angus Pollard%'; # 8988
SELECT * FROM teams WHERE name LIKE '%Story Terrace%'; # 379, 307, 12894
SELECT * FROM crm_fields WHERE crm_configuration_id = 307 and object_type != 'opportunity';
select * from contacts where team_id = 379 and name like '%bebro%'; # 5874411, crm: 77229348507
SELECT * FROM crm_field_data WHERE object_id = 5874411;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 379
and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%mentio%'; # 117, 94, 6371, [EMAIL]
SELECT * FROM activities WHERE uuid_to_bin('82939311-1af0-4506-8546-21e8d1fdf2c1') = uuid;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Tourlane%'; # 187, 209, 8150, [EMAIL]
SELECT * FROM opportunities WHERE team_id = 187 and crm_provider_id = '006Se000008xfvNIAQ'; # 3537793
select * from generic_ai_prompts where subject_id = 3537793;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Lunio%'; # 120, 97, 10984, [EMAIL]
SELECT * FROM crm_configurations WHERE id = 97;
SELECT * FROM crm_layouts WHERE crm_configuration_id = 97;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 355;
SELECT * FROM crm_fields WHERE id = 32682;
select cfd.value, o.* from opportunities o
join crm_field_data cfd on o.id = cfd.object_id and cfd.crm_field_id = 32682
where team_id = 120
and cfd.value != ''
;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 120
and sa.provider = 'salesforce';
select * from opportunities where team_id = 120 and crm_provider_id = '006N1000007X8MAIA0';
SELECT * FROM crm_field_data WHERE object_id = 2313439;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE id = 410;
SELECT * FROM teams WHERE name LIKE '%Local Business Oxford%';
select * from scorecards where team_id = 410;
select * from scorecard_rules;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Funding%'; # 220, 177, 8603, [EMAIL]
select * from activities a
join opportunities o on a.opportunity_id = o.id
join users u on o.user_id = u.id
where a.crm_configuration_id = 177 and a.type LIKE '%email-out%'
# and a.actual_end_time > '2024-12-16 00:00:00'
# and o.remotely_created_at > '2024-12-01 00:00:00'
# and u.group_id = 1014
and u.id = 9021
order by a.id desc;
SELECT * FROM opportunities WHERE id in (3981384,4017346);
SELECT * FROM users WHERE team_id = 220 and id IN (8775, 11435);
select * from users where id = 9021;
select * from inboxes where user_id = 9021;
select * from inbox_emails where inbox_id = 1349 and email_date > '2024-12-18 00:00:00';
select * from email_messages where team_id = 220
and orig_date > '2024-12-16 00:00:00' and orig_date < '2024-12-19 00:00:00'
and subject LIKE '%Personal%'
# and 'from' = '[EMAIL]'
;
select * from activities a
join opportunities o on a.opportunity_id = o.id
where a.user_id = 9021 and a.type LIKE '%email-out%'
and a.actual_end_time > '2024-12-18 00:00:00'
and o.user_id IS NOT NULL
and o.remotely_created_at > '2024-12-01 00:00:00'
order by a.id desc;
SELECT * FROM opportunities WHERE team_id = 220 and name LIKE '%Right Car move Limited%' and id = 3966852;
select * from activities where crm_configuration_id = 177 and type LIKE '%email%' and opportunity_id = 3966852 order by id desc;
select * from team_settings where name IN ('useCloseDate');
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Hurree%'; # 104, 81, 6175, [EMAIL]
SELECT * FROM opportunities WHERE team_id = 104 and name = 'PropOp';
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 104
and sa.provider = 'hubspot';
select * from crm_configurations where last_synced_at > '2025-01-19 01:00:00'
select * from teams where crm_id IS NULL;
select t.name as 'team', u.name as 'owner', u.email, u.phone
from teams t
join activity_providers ap on t.id = ap.team_id
join users u on t.owner_id = u.id
where 1=1
and t.status = 'active'
and ap.is_enabled = 1
# and u.status = 1
and ap.provider = 'ms-teams';
select * from crm_configurations where provider = 'bullhorn'; # 344
SELECT * FROM teams WHERE id = 442; # 14293
select * from users where team_id = 442;
select * from social_accounts sa where sa.sociable_id = 14293;
select * from invitations where team_id = 442;
# [PASSWORD_DOTS]
SELECT * FROM users WHERE email LIKE '%[EMAIL]%'; # 14022
SELECT * FROM teams WHERE id = 429;
select * from opportunities where team_id = 429 and crm_provider_id IN (16157415775, 22246219645);
select * from activities where opportunity_id in (4340436,4353519);
select * from transcription where activity_id IN (25630961,25381771);
select * from generic_ai_prompts where subject_id IN (4353519);
SELECT
a.id as activity_id,
a.opportunity_id,
a.type as activity_type,
a.language,
CONCAT(a.title, a.description) AS mail_content,
e.from AS mail_from,
e.to AS mail_to,
e.subject AS mail_subject,
e.body AS mail_body,
p.type as prompt_type,
p.status as prompt_status,
p.content AS prompt_content,
a.actual_start_time as created_at
FROM activities a
LEFT JOIN ai_prompts p ON a.transcription_id = p.transcription_id AND p.deleted_at IS NULL
LEFT JOIN email_messages e ON a.id = e.activity_id
WHERE a.actual_start_time > '2024-01-01 00:00:00'
AND a.opportunity_id IN (4353519)
AND a.status IN ('completed', 'received', 'delivered')
AND a.deleted_at IS NULL
AND a.type NOT IN ('sms-inbound', 'sms-outbound')
ORDER BY a.opportunity_id ASC, a.id ASC;
SELECT * FROM users WHERE name LIKE '%George Fierstone%'; # 14293
SELECT * FROM teams WHERE id = 442;
SELECT * FROM crm_configurations WHERE id = 344;
select * from team_features where team_id = 442;
select * from groups where team_id = 442;
select * from playbooks where team_id = 442;
select * from playbook_categories where playbook_id = 1729;
select * from crm_fields where crm_configuration_id = 344 and id = 172024;
SELECT * FROM crm_field_values WHERE crm_field_id = 172024;
select * from crm_layouts where crm_configuration_id = 344;
select * from playbook_layouts where playbook_id = 1729;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 221, 9444
select s.*
# , s.sent_at, u.name, a.*
from activity_summary_logs s
inner join activities a on a.id = s.activity_id
inner join users u on u.id = a.user_id
where a.crm_configuration_id = 356
and s.sent_at > date_sub(now(), interval 60 day)
order by a.actual_end_time desc;
select * from activities a
# inner join activity_summary_logs s on s.activity_id = a.id
where a.crm_configuration_id = 356 and a.actual_end_time > date_sub(now(), interval 60 day)
# and a.crm_provider_id is not null
# and provider <> 'ringcentral'
and status = 'completed'
order by a.actual_end_time desc;
select * from teams order by id desc; # 17328, 32, 17830, [EMAIL]
SELECT * FROM users;
SELECT * FROM users where team_id = 260 and status = 1; # 201 - 150 active
SELECT * FROM teams WHERE id = 260;
select * from team_settings where team_id = 260;
select * from crm_configurations where team_id = 260;
SELECT * FROM crm_layouts WHERE crm_configuration_id = 356;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1184;
select * from accounts where crm_configuration_id = 221 order by id desc; # 7000
select * from leads where crm_configuration_id = 221 order by id desc; # 0
select * from contacts where crm_configuration_id = 221 order by id desc; # 200 000
select * from opportunities where crm_configuration_id = 221 order by id desc; # 0
select * from crm_profiles where crm_configuration_id = 221 order by id desc; # 23
select * from crm_fields where crm_configuration_id = 221;
select * from crm_field_values where crm_field_id = 5302 order by id desc;
select * from crm_layouts where crm_configuration_id = 221 order by id desc;
select * from stages where crm_configuration_id = 221 order by id desc;
select * from accounts where crm_configuration_id = 356 order by id desc; # 7000
select * from leads where crm_configuration_id = 356 order by id desc; # 0
select * from contacts where crm_configuration_id = 356 order by id desc; # 200 000
select * from opportunities where crm_configuration_id = 356 order by id desc; # 0
select * from crm_profiles where crm_configuration_id = 356 order by id desc; # 23
select * from crm_fields where crm_configuration_id = 356;
select * from crm_field_values where crm_field_id = 5302 order by id desc;
select * from crm_layouts where crm_configuration_id = 356 order by id desc;
select * from stages where crm_configuration_id = 356 order by id desc;
select * from playbooks where team_id = 260 order by id desc; # 4 (2 deleted)
select * from groups where team_id = 260 order by id desc; # 27 groups, (2 deleted)
select * from playbook_layouts where playbook_id IN (1410,1409,1276,1254); # 4
select ce.* from calendars c
join users u on c.user_id = u.id
join calendar_events ce on c.id = ce.calendar_id
where u.team_id = 260
and (ce.start_time > '2025-02-21 00:00:00')
;
# calendar events 1207
#
select * from opportunities where team_id = 260;
SELECT * FROM crm_field_data WHERE object_id = 4696496;
select * from activities where crm_configuration_id = 356 and crm_provider_id IS NOT NULL;
select * from activities where crm_configuration_id IN (221) and provider NOT IN ('ms-teams', 'uploader', 'zoom-bot')
# and type = 'conference' and status = 'scheduled' and activities.is_internal = 0
and created_at > '2024-03-01 00:00:00'
order by id desc; # 880 000, ringcentral, avaya
SELECT * FROM participants WHERE activity_id = 26371744;
# all activities 942 000 +
# conference 7385 - scheduled 984 - external 343
select * from activities where id = 26321812;
select * from participants where activity_id = 26321812;
select * from participants where activity_id in (26414510,26414514,26414516,26414604,26414653,26414655);
select * from leads where id in (720428,689175,731546,645866,621037);
select * from users where id = 13841;
select * from opportunities where user_id = 9541;
select * from stages where id = 15900;
select * from accounts where
# id IN (4160055,5053725,4965303,4896434)
id in (4584518,3249934,3218025,3891133,3399450,4172999,4485161,3101785,4587203,3070816,2870343,2870341,3563940,4550846,3424464,3249963,2870342)
;
select * from activities where id = 26654935;
SELECT * FROM opportunities WHERE id = 4803458;
SELECT * FROM opportunities where team_id = 260 and user_id = 13841 AND stage_id = 15900;
SELECT id, uuid, provider, type, lead_id, account_id, contact_id, opportunity_id, stage_id, status, recording_state, title, actual_start_time, actual_end_time
FROM activities WHERE user_id = 13841 AND opportunity_id IN (4729783, 4731717, 4731726, 4732064, 4732849, 4803458, 4813213);
SELECT DISTINCT
o.id, o.stage_id, s.name, a.title,
a.*
FROM activities a
# INNER JOIN tracks t ON a.id = t.activity_id
INNER JOIN users u ON a.user_id = u.id
INNER JOIN teams team ON u.team_id = team.id
INNER JOIN groups g ON u.group_id = g.id
INNER JOIN opportunities o ON a.opportunity_id = o.id
INNER JOIN stages s ON o.stage_id = s.id
WHERE
a.crm_configuration_id = 356
AND a.status IN ('completed', 'failed')
AND a.recording_state != 'stopped'
# and a.user_id = 13841
AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')
AND team.uuid = uuid_to_bin('a607fba7-452e-4683-b2af-00d6cb52c93c')
AND g.uuid = uuid_to_bin('b5d69e40-24a0-4c16-810b-5fa462299f94')
AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
# AND t.type IN ('audio', 'video')
AND (
(a.actual_start_time BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59')
OR
(
a.actual_start_time IS NULL
AND a.type IN ('sms-outbound', 'sms-inbound')
AND a.created_at BETWEEN '2025-03-13 00:00:00' AND '2025-03-18 07:59:59'
)
)
AND (
a.is_private = 0
OR (
a.is_private = 1
AND u.uuid = uuid_to_bin('6f40e4b8-c340-4059-b4ac-1728e87ea99e')
)
)
AND (
# s.id = 15900
s.uuid = uuid_to_bin('04ca1c26-c666-4268-a129-419c0acffd73')
OR s.uuid IS NULL -- Include records without opportunity stage
)
ORDER BY a.actual_end_time DESC;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Lead Forensics%'; # 190, 162, 8474, [EMAIL]
SELECT * FROM users WHERE team_id = 190;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 190
and sa.provider = 'hubspot';
select * from role_user where user_id = 8474;
select * from crm_configurations where provider = 'bullhorn';
SELECT * FROM opportunities WHERE uuid_to_bin('94578249-65ec-4205-90f2-7d1a7d5ab64a') = uuid;
SELECT * FROM users WHERE uuid_to_bin('26dbadeb-926f-4150-b11b-771b9d4c2f9a') = uuid;
SELECT * FROM opportunities WHERE id = 4732493;
select * from activities where opportunity_id = 4732493;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE id = 443; # 358, 14315, [EMAIL]
SELECT * FROM opportunities WHERE team_id = 443;
SELECT a.id, a.type, a.user_id, a.status, a.deleted_at, u.name, u.email, u.team_id as activity_team_id, u.status, u.deleted_at, t.name, t.status, s.team_id as stage_team_id
FROM activities AS a
JOIN stages AS s ON a.stage_id = s.id
JOIN users AS u ON u.id = a.user_id
JOIN teams AS t ON t.id = s.team_id
WHERE u.team_id <> s.team_id and t.id > 135;
SELECT
crm_configuration_id,
crm_provider_id,
COUNT(*) as duplicate_count,
GROUP_CONCAT(id) as stage_ids,
GROUP_CONCAT(name) as stage_names
FROM stages
GROUP BY crm_configuration_id, crm_provider_id
HAVING COUNT(*) > 1
ORDER BY duplicate_count DESC;
select * from stages where id IN (14898,14907);
select * from business_processes;
SELECT *
FROM crm_configurations
WHERE team_id IN (
SELECT team_id
FROM crm_configurations
GROUP BY team_id
HAVING COUNT(*) > 1
)
ORDER BY team_id;
SELECT *
FROM teams
WHERE crm_id IN (
SELECT crm_id
FROM teams
GROUP BY crm_id
HAVING COUNT(*) > 1
)
ORDER BY crm_id;
# [PASSWORD_DOTS]
select * from crm_configurations where provider = 'integration-app';
SELECT * FROM teams WHERE id = 443; # Correre Naturale 358 14315 [EMAIL]
select * from activities where crm_configuration_id = 358 order by actual_end_time desc;
select id, uuid, actual_end_time, crm_provider_id, is_internal, playbook_category_id, type, user_id, lead_id, contact_id, account_id, opportunity_id, status, title from activities where crm_configuration_id = 358 order by actual_end_time desc;
select * from team_features where team_id = 358;
select * from activity_summary_logs;
select * from teams where id = 406;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Sportfive%'; # 267, 202, 14637, [EMAIL]
select * from activities where crm_configuration_id = 202 order by actual_end_time desc;
SELECT * FROM users where id = 14637;
SELECT * FROM teams where id = 267;
SELECT * FROM groups where id = 1118;
select g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a
inner join users u on u.id = a.user_id
inner join groups g on g.id = u.group_id
where a.crm_configuration_id = 202
and a.is_internal = 0
and (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')
and a.type = 'conference'
and a.status != 'completed'
and a.external_id is not null
order by a.scheduled_start_time desc;
SELECT * FROM activities
WHERE crm_configuration_id = 202
AND status IN ('completed', 'failed')
AND recording_state != 'stopped'
AND type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
AND (is_private = 0 OR user_id = 14637)
AND (
(
actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'
) OR (
actual_start_time IS NULL
AND type IN ('sms-outbound', 'sms-inbound')
AND created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'
)
)
AND NOT EXISTS (
SELECT 1
FROM tracks
WHERE
tracks.activity_id = activities.id
AND tracks.type IN ('audio', 'video')
)
ORDER BY actual_end_time DESC;
SELECT DISTINCT
a.*
FROM activities a
INNER JOIN tracks t ON a.id = t.activity_id
INNER JOIN users u ON a.user_id = u.id
INNER JOIN teams team ON u.team_id = team.id
WHERE
a.crm_configuration_id = 202
AND a.status IN ('completed', 'failed')
AND a.recording_state != 'stopped'
# and a.user_id = 14637
AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
# AND t.type IN ('audio', 'video')
AND (
(a.actual_start_time BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59')
OR
(
a.actual_start_time IS NULL
AND a.type IN ('sms-outbound', 'sms-inbound')
AND a.created_at BETWEEN '2025-03-12 12:00:00' AND '2025-03-24 11:59:59'
)
)
AND (
a.is_private = 0
OR (
a.is_private = 1
AND a.user_id = 14637
)
)
ORDER BY a.actual_end_time DESC
;
SELECT DISTINCT a.*
FROM activities a
INNER JOIN users u ON a.user_id = u.id
INNER JOIN teams t ON u.team_id = t.id
# INNER JOIN tracks tr ON a.id = tr.activity_id
# INNER JOIN groups g ON u.group_id = g.id
WHERE 1=1
AND t.id = 267
# AND t.uuid = uuid_to_bin('aed4927b-f1ea-499e-94c3-83762fd233e8')
AND a.status IN ('completed', 'failed')
AND a.recording_state != 'stopped'
AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
# AND tr.type NOT IN ('audio', 'video')
AND (
a.is_private = 0
OR a.user_id = 14637
)
AND (
(a.actual_start_time BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59')
OR (
a.actual_start_time IS NULL
AND a.type IN ('sms-outbound', 'sms-inbound')
AND a.created_at BETWEEN '2025-03-19 00:00:00' AND '2025-03-21 23:59:59'
)
)
# and NOT EXISTS (
# SELECT 1
# FROM tracks t
# WHERE t.activity_id = a.id
# AND t.type IN ('audio', 'video')
# )
ORDER BY a.actual_end_time DESC;
SELECT * FROM tracks WHERE activity_id = 26485995;
select a.is_private, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a
inner join users u on u.id = a.user_id
where a.crm_configuration_id = 202
# and a.is_internal = 0
and (a.actual_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')
and a.type IN ("softphone","softphone-inbound","conference","sms-inbound")
and a.status IN ('completed', 'failed')
# and a.external_id is not null
order by a.actual_end_time desc;
select * from activities a where a.crm_configuration_id = 202
and a.actual_start_time between '2025-03-20 00:00:00' and '2025-03-21 00:00:00'
# AND a.type IN ('softphone', 'softphone-inbound', 'conference', 'sms-inbound', 'sms-outbound')
select g.name, a.title, uuid_from_bin(a.uuid), a.external_id, a.status, a.recording_state, a.recording_reason_code, a.scheduled_start_time, a.scheduled_end_time, a.actual_start_time, a.actual_end_time from activities a
inner join users u on u.id = a.user_id
inner join groups g on g.id = u.group_id
where a.crm_configuration_id = 202
and a.is_internal = 0
and (a.scheduled_start_time between '2025-03-19 00:00:00' and '2025-03-21 00:00:00')
and a.type = 'conference'
and a.status != 'completed'
and a.external_id is not null
order by a.scheduled_start_time desc;
SELECT * FROM teams WHERE name LIKE '%Tourlane%';
SELECT * FROM crm_fields WHERE crm_configuration_id = 209 and object_type = 'opportunity';
SELECT * FROM crm_field_data WHERE crm_field_id = 98809;
select * from users where status = 1 AND timezone = 'MDT';
select * from opportunities where id = 3769814;
select * from deal_risks where opportunity_id = 3769814;
select cp.* from crm_profiles cp
join users u on cp.user_id = u.id
join crm_configurations crm on cp.crm_configuration_id = crm.id
where crm.provider = 'hubspot' AND u.status = 1 AND log_notes != 'none';
select * from crm_fields where id = 154575;
select * from team_features where feature = 'SUPPORTS_SYNC_MISSING_CALL_DISPOSITIONS';
SELECT * FROM teams WHERE id = 176; # crm 148
select * from activities where crm_configuration_id = 148 and provider = 'hubspot' order by id desc;
select * from activity_providers where provider = 'amazon-connect';
select * from crm_fields cf
join crm_configurations crm on crm.id = cf.crm_configuration_id
where crm.provider = 'hubspot' and cf.object_type IN ('account', 'contact');
# [PASSWORD_DOTS]
SELECT * FROM users WHERE id IN (15415, 15418);
SELECT * FROM groups WHERE id IN (1805,1806);
SELECT * FROM playbooks WHERE id = 1860;
SELECT * FROM playbook_categories WHERE id = 38634;
SELECT * FROM crm_fields WHERE id = 189962;
SELECT * FROM teams WHERE name = 'Pulsar Group'; # 472, 380, 15138 [EMAIL]
SELECT * FROM crm_profiles WHERE user_id = 15415;
SELECT * FROM social_accounts WHERE sociable_id = 15415 and provider = 'salesforce';
select * from sidekick_settings where team_id = 472;
SELECT * FROM activities WHERE uuid_to_bin('452c58c7-b87c-4fdd-953e-d7af185e9588') = uuid; # 28617536, user: 15418
SELECT * FROM activities WHERE uuid_to_bin('399114ee-d3a8-458c-bff5-5f654658db0a') = uuid; # 28344407, user: 15415
SELECT * FROM activities WHERE uuid_to_bin('f0aa567f-0ab1-4bbb-96aa-37dcf184676b') = uuid; # 28580288, user: 15415
SELECT * FROM activities WHERE uuid_to_bin('50c086b1-2770-4bca-b5ae-6bac22ec426b') = uuid; # 28566069, user: 15415
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%TeamTailor%'; # 109, 218, 13969, [EMAIL]
select * from crm_configurations where id = 218;
SELECT * FROM activities WHERE uuid_to_bin('e39b5857-7fdb-4f5a-951a-8d3ca69bb1b0') = uuid; # 28338765
SELECT * FROM users WHERE id IN (13232, 13230);
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 109
and sa.provider = 'salesforce';
0057R00000EPL5HQAX Inez Ekblad
1091cb81-5ea1-4951-a0ed-f00b568f0140 Triman Kaur
SELECT * FROM crm_profiles WHERE user_id IN (13232, 13230);
############################################################################################
SELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939 00UVg00000FLvnSMAT
SELECT * FROM crm_field_data WHERE activity_id = 28655939;
SELECT * FROM crm_fields WHERE id IN (94491,94493,94498);
SELECT * FROM users WHERE id = 13658;
SELECT * FROM teams WHERE id = 109;
SELECT * FROM crm_configurations WHERE id = 218;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 109
and sa.provider = 'salesforce';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Strengthscope%'; # 481, 390, 15420, [EMAIL]
SELECT * FROM stages WHERE crm_configuration_id = 390;
select * from business_processes where team_id = 481 and crm_configuration_id = 390;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 481
and sa.provider = 'salesforce';
SELECT * FROM users WHERE id = 15780; # team 462
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 462
and sa.provider = 'hubspot';
select * from teams where id = 495;
SELECT * FROM users WHERE id = 15794;
select * from social_accounts where sociable_id = 15794;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Flight%'; # 427, 333, 13752
SELECT * FROM accounts WHERE team_id = 427 and crm_provider_id = '668731000183444517';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Group GTI%'; # 495, 407, 15794
SELECT * FROM activities WHERE crm_configuration_id = 407
and status = 'completed' and type = 'conference'
order by id desc;
select ru.*, pr.*, p.* from users u join role_user ru on ru.user_id = u.id
join permission_role pr on pr.role_id = ru.role_id
join permissions p on p.id = pr.permission_id
where team_id = 495 and p.name IN ('dial');
select * from permission_role;
select * from activities where crm_configuration_id = 407 and status = 'completed' order by id desc;
SELECT * FROM activities WHERE id = 29512773;
SELECT * FROM activities WHERE id IN (29042721,28991325,29002874);
SELECT al.* from activity_summary_logs al join activities a on a.id = al.activity_id
where a.crm_configuration_id = 407
# and a.id IN (29042721,28991325,29002874);
SELECT * FROM users WHERE id = 15794;
SELECT * FROM users WHERE team_id = 495;
SELECT * FROM social_accounts WHERE sociable_id = 15794;
SELECT * FROM opportunities WHERE team_id = 495 and name like '%OC:%';
SELECT * FROM contacts WHERE team_id = 495;
SELECT * FROM leads WHERE team_id = 495;
SELECT * FROM accounts WHERE team_id = 495;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 407;
SELECT * FROM crm_fields WHERE crm_configuration_id = 407;
SELECT * FROM crm_configurations WHERE id = 407;
SELECT * FROM opportunities WHERE team_id = 495 and close_date BETWEEN '2025-06-01' AND '2025-07-01'
and user_id IS NOT NULL and is_closed = 1 and is_won = 1;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Hamilton Court FX LLP%'; # 249, 187, 10103
SELECT * FROM activities WHERE uuid_to_bin('4659c2bb-9a49-484e-9327-a3d66f1e028c') = uuid; # 28951064
SELECT * FROM crm_fields WHERE crm_configuration_id = 187 and object_type IN ('tasks', 'event');
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Checkstep%'; # 325, 256, 11753
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 325
and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid; # 28611085
SELECT * FROM activities WHERE uuid_to_bin('980f0336-840b-4185-a5a9-30cf8b0749a8') = uuid; # 28719733
SELECT * FROM activity_summary_logs where activity_id = 28719733;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Learning%'; # 260, 356, 9444
SELECT * FROM activity_summary_logs where sent_at BETWEEN '2025-06-09 11:38:00' AND '2025-06-09 11:40:00';
SELECT * FROM leads WHERE crm_configuration_id = 356 and crm_provider_id = '230045001502770504'; # 823630
select * from activities where crm_configuration_id = 356 and lead_id = 841732;
SELECT * from activity_summary_logs al join activities a on a.id = al.activity_id
where a.crm_configuration_id = 356;
select * from activities where crm_configuration_id = 356
and actual_end_time between '2025-06-09 11:00:00' and '2025-06-09 12:00:00'
order by id desc;
select * from accounts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;
select * from leads where crm_configuration_id = 356 and crm_provider_id = '230045001514275654' order by id desc;
select * from contacts where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;
select * from opportunities where crm_configuration_id = 356 and crm_provider_id = '230045001514403366' order by id desc;
select * from team_features where team_id = 260;
select * from features where id IN (1,2,4,6,18,19,20,9,10,3,23,24,25,26,27);
SELECT * FROM activities WHERE uuid_to_bin('7be372e2-1916-4d79-a2f3-ca3db1346db3') = uuid;
select * from crm_fields;
select * from crm_layout_entities;
SELECT * FROM teams WHERE name LIKE '%Optable%';
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Teamtailor%'; # 109, 218, 13969
SELECT * FROM crm_configurations WHERE id = 218;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 109
and sa.provider = 'salesforce';
SELECT * FROM activities WHERE uuid_to_bin('675eeaeb-5681-42db-90bc-54c07a604408') = uuid; # 28655939
SELECT * FROM crm_field_data WHERE activity_id = 28655939;
SELECT * FROM crm_fields WHERE id in (94491,94493,94498);
select * from teams where crm_id IS NULL;
SELECT * FROM activities WHERE uuid_to_bin('71aa8a0c-9652-4ff6-bee7-d98ae60abef6') = uuid;
# [PASSWORD_DOTS]
select * from team_domains where team_id = 399;
SELECT * FROM teams WHERE name LIKE '%Rydoo%'; # 399, 318, 13207
select * from calendar_events where id = 5163781;
SELECT * FROM activities WHERE uuid_to_bin('be2cbc52-7fda-46a0-9ae0-25d9553eafc0') = uuid; # 29443896
SELECT * FROM participants WHERE activity_id = 29443896;
select * from contacts where crm_configuration_id = 318 and email = '[EMAIL]';
select * from leads where crm_configuration_id = 318 and email = '[EMAIL]';
select * from activities where user_id = 14937 order by created_at ;
select * from users where id = 14937;
select * from contacts where crm_configuration_id = 318 and email LIKE '%@strawberry.se';
select * from opportunities where crm_configuration_id = 318 and crm_provider_id = '006Sf00000D1WOAIA3';
select * from activities a join participants p on a.id = p.activity_id
where crm_configuration_id = 318 and a.updated_at > '2025-06-23T08:18:43Z';
# [PASSWORD_DOTS]
SELECT * FROM opportunities WHERE team_id = 379 and crm_provider_id = '39334518886';
SELECT * FROM opportunities WHERE team_id = 379 order by id desc;
SELECT * FROM teams WHERE id = 379;
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 379 and sociable_id = 13852
and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE id = 307;
SELECT * FROM crm_layouts WHERE crm_configuration_id = 307;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1027;
SELECT * FROM crm_fields WHERE crm_configuration_id = 307
and id IN (144750,144855,145158,155227);
SELECT * FROM activities;
select * from activities
where created_at > '2025-07-01 00:00:00'
# and created_at < '2025-08-01 00:00:00'
and type not in ('email-outbound', 'email-inbound')
and account_id is null
and contact_id is null
and lead_id is null
and opportunity_id is not null
;
SELECT * FROM activities WHERE id IN (25344155, 25344296, 25501909, 28692187);
SELECT * FROM crm_configurations WHERE id in (335,301,200);
select * from crm_fields where crm_configuration_id = 230 and crm_provider_id = 'Age2__c';
SELECT * FROM teams WHERE name LIKE '%Resights%';
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_configurations where provider = 'bullhorn'; # 344
select * from teams where id IN (442);
select * from activities
where crm_configuration_id = 177
and provider = 'amazon-connect'
order by id desc;
# and source <> 'gong';
select * from activity_providers where provider = 'amazon-connect';
SELECT * FROM activities WHERE uuid_to_bin('cec1993b-a7e5-4164-b74d-d680ea51d2f2') = uuid;
select * from crm_configurations where store_transcript = 1;
SELECT * FROM teams WHERE id IN (80);
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Sedna%'; # 277, 213, 12594
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 277
and sa.provider = 'salesforce';
select * from activities where crm_configuration_id = 213 and account_id = 2511502;
select * from crm_configurations where id = 213;
SELECT * FROM activities WHERE uuid_to_bin('35aa790a-8569-4544-8268-66f9a4a26804') = uuid; # 33981604
SELECT * FROM participants WHERE activity_id = 33981604;
SELECT * FROM crm_fields WHERE crm_configuration_id = 337 and object_type = 'task';
select * from social_accounts sa
join users u on sa.sociable_id = u.id
where u.team_id = 431
and sa.provider = 'salesforce';
SELECT * FROM activities WHERE uuid_to_bin('b5476c7d-19a8-491b-869d-676ea1e857b6') = uuid; # 33997223
select * from activity_summary_logs where activity_id = 33997223;
select * from activity_notes where activity_id = 33997223;
# [PASSWORD_DOTS]
SELECT * FROM teams WHERE name LIKE '%Abode%';
select * from features;
select * from teams t
where t.status = 'active'
and id NOT IN (select team_id from team_features where feature_id = 9)
;
select * from playbook_layouts where playbook_id = 1725;
SELECT * FROM activities WHERE uuid_to_bin('65cc283c-4849-49e6-927f-4c281c8fea19') = uuid; # 34297473
select * from teams where id = 318;
select * from crm_configurations where team_id = 318;
select * from playbooks where team_id = 318;
SELECT * FROM crm_layouts where crm_configuration_id = 381;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1259;
SELECT * FROM crm_fields WHERE id IN (192938,192936,192939);
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1266;
SELECT * FROM crm_fields WHERE id IN (192980,192991,192997,192998,193064,193067);
SELECT * FROM activities WHERE uuid_to_bin('a902289b-285c-48eb-9cc2-6ad6c5d938f5') = uuid; # 34297533
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;
SELECT * FROM crm_fields WHERE id IN (131668,131669,131670,131671,131676,131797);
SELECT * FROM teams WHERE name LIKE '%Peripass%'; # 351, 281, 12124
select * from crm_layouts where crm_configuration_id = 281;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 927;
select * from crm_fields where crm_configuration_id = 281 and id in (131668,131669,131670,131671,131676,131797);
select * from opportunities where crm_configuration_id = 281;
SELECT * FROM activities WHERE id IN (34211315, 34130075);
SELECT * FROM crm_field_data WHERE object_id IN (34211315, 34130075);
select cf.crm_configuration_id, cle.crm_layout_id, cle.id, cf.id from crm_field_data cfd
join crm_layout_entities cle on cle.id = cfd.crm_layout_entity_id
join crm_fields cf on cle.crm_field_id = cf.id
where cf.deleted_at IS NOT NULL
GROUP BY cle.id, cf.id;
select * from crm_layouts where id IN (355);
select u.email, t.crm_id, t.* from teams t
join users u on u.id = t.owner_id
where crm_id IN (97);
SELECT * FROM crm_fields WHERE id = 9...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42574
|
1560
|
7
|
2026-05-14T11:42:49.812648+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778758969812_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybookCreated.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormcodeFV faVsco.js°9 JY-20904-fix-update-es- PhostormcodeFV faVsco.js°9 JY-20904-fix-update-es-orProiectOpportunities• @ Playbooks•Playoookcreatea.onosms-relay-failed.blade.phc Playoookupaatea.onpD Playlistsw sidekick0 Teams(C) UpdateSinaleEntity.phoC) MatchActivityermData.php(C) Service.php© Client.phpa Transcription(C) PayloadBuilder.ohdC) Confiquration.php0 Usersk?phpC Event.php© EventDispatcher.phpnamesnace Jminnv Events Plavhooks.v L Exceptions0 crm› use ...() ActivitvProvidersxceotion© ApiException.php()AniResponseSxcention.orclass PlaybookCreated• ApplicationException.php(4) AsvncslasticSearchUodaluse SenializesModels:© BadKeywordsQueryExcef 13ConfigurationException.p( CrmSycention nhn* The playbook instance@ CrmUpdateException.pnp 16© DatabaseException.php* avar Playbook@ DealRisksException.ong© DomainException.phppublic Playbook splaybook@ EmailActivitvimportExcep© ErrorDownloadingEmptyF( ErrorDownloadingFileExc* The user who created the plaubook.( FileNotFoundException.pl* avar User© FileSystemException.phpc) Handler.phpoublic User Suser:HttpBadRequestExceptiolHitpbadRequestWitherrolHttpForbiddenException.* Crente n new event instance.( Hitomethod NotA lowedznublic function construetPlavbook Solavbook. Usen Suser)...?(5) HttoSessionExoired Excer() HttoUnauthorized Exceoti( HttoUnsuoportedFormat:[EMAIL] InvalidFileEycention.ohn@ [EMAIL]@l caicfycention.nhnC MayTeamTrialSizeSycepo9 ModolNotEoundEvcontior@ NoDoculteEycontion nhn@ NotimnlomantodEvcontin© NotSupportedException.g© TextRelayService.phg(©) ValidateSendingMessage.pnp© ConterencecrmmatcherJob.ong© OpportunityRepositorv.phgOpportunitySyncTrait.pho=custom.logscratch. &.ison= laravel.logA SF jiminny@localhost]& HS_local [jiminny@localhost]& console [PROD]A console (EU] X iii accounts [EU]fii stages (EU]tid teams [EU]ImporbotkecoraingJoo.org© Activity.php© FixActivitiesOpportunity.php© Opportunity.php& console SlAGiNGTy: AutoyPlavaroundySo jiminnym 030 A9 A27 X3 X106^.SELE * FROM users WhERE name LIKE "%rell Hovles': # 1651SELECT * FROM social accounts WHERE sociable id = 17651:116431| x16461645 v1646SELEC * FROM activities WERE uuid to bin('975c6830-7049-4cle-b2e9-ac80c10a738a') = uuid:=1647SELEC * FROM opportunities WHEREid IN (7842553, 6211727):SELECT * FROM contacts WHERE id IN (10202724, 6211727):SELE * FROM opportunity stages WHERE opportunity 1d = 7842553°SSLSMCONCAT(u.id, CASE WHEN U.id = t.owner_id THEN ' (owner)' ELSE'' END) AS user_idv.emailt.owner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_idWHERE u.team_id = 519 and sa.provider = 'hubspot';select * from crm confiqurations where id = 436:select * from crm profiles where crm confiquration id = 436: # 76091797 -> 16612select * from contact roles where contact id = 10202724:select * trom stages whereteam_id = 519: # 187781O SSELECTcrm provider 1d.stage 1d.is closed.is won.stage undated at.undated atFROM opportunitiesWHERE id TN 6211727. 7842553)4CELSCT * SP0M onnontunity contacteWHEPE onnontunitv id = 4211727 AN0 contact id - 1920272/•1678SELECT id, name, stage id, is closed, is won, updated at, remotely created atSP0M onnontuni+joc1682WHERE account id = 8179134npnEp Ry undatod at necr.• suppont Dally • In 10m100% L2. Thu 14 May 14:42:49AskJiminnyReportActivityServiceTest vcascadePipedrive SDK EvaluaFixing Activity Stage Dare you sure about that? Check https://github.com/pipedrive/client-php/blob/master/CHANGELOG.mdThoughtsCreated outline = raw.githubusercontent.comThought for 1sYou're absolutelv riaht - I was incorrect. Let me correct the information:Corrected SDK Last Update Datespipedrive/client-pho (Officia):Latest Version: 15.4.0• Last Updated: February 16, 2026•Link: https:/github.com/pipedrive/client-php/lolob/master/changeLog.mddeviolpipedrive (IsraelOrtuno:• Latest Version: 2.17.0•Last Updated: November 10, 2024Link: httos://aithub.com/israelOrtuno/oioedrive/releases/taa/2.17.0Undated RecommendationiThe ofticial SDK is signiticantly more recent (Feb 2026 vs Nov 2024) and actively maintained with trequent releases. ThisOtticial SDK Advantages NoW:•Much more recent updates (3 months ago vs 14 months ago)Very active develooment (15+ maior versions since May 2024)•Official Pipedrive support• API v2 support (critical for future-proofing)• OpenAPI-generated (automatic APl updates)• SIl more comolex AP desianNo Laravel intearation•Migration complexity remains hichThe official SDK's recent activity and API v2 support make it more compelling, but the migration effort and lack of Laraveintearation are still sianiticant tactorsYour included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Ask anvthina (84-L1« Code SWF-1.6W Windsurf Teamc1-1UTE.8io 4 spaces...
|
NULL
|
-386338267981720021
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormcodeFV faVsco.js°9 JY-20904-fix-update-es- PhostormcodeFV faVsco.js°9 JY-20904-fix-update-es-orProiectOpportunities• @ Playbooks•Playoookcreatea.onosms-relay-failed.blade.phc Playoookupaatea.onpD Playlistsw sidekick0 Teams(C) UpdateSinaleEntity.phoC) MatchActivityermData.php(C) Service.php© Client.phpa Transcription(C) PayloadBuilder.ohdC) Confiquration.php0 Usersk?phpC Event.php© EventDispatcher.phpnamesnace Jminnv Events Plavhooks.v L Exceptions0 crm› use ...() ActivitvProvidersxceotion© ApiException.php()AniResponseSxcention.orclass PlaybookCreated• ApplicationException.php(4) AsvncslasticSearchUodaluse SenializesModels:© BadKeywordsQueryExcef 13ConfigurationException.p( CrmSycention nhn* The playbook instance@ CrmUpdateException.pnp 16© DatabaseException.php* avar Playbook@ DealRisksException.ong© DomainException.phppublic Playbook splaybook@ EmailActivitvimportExcep© ErrorDownloadingEmptyF( ErrorDownloadingFileExc* The user who created the plaubook.( FileNotFoundException.pl* avar User© FileSystemException.phpc) Handler.phpoublic User Suser:HttpBadRequestExceptiolHitpbadRequestWitherrolHttpForbiddenException.* Crente n new event instance.( Hitomethod NotA lowedznublic function construetPlavbook Solavbook. Usen Suser)...?(5) HttoSessionExoired Excer() HttoUnauthorized Exceoti( HttoUnsuoportedFormat:[EMAIL] InvalidFileEycention.ohn@ [EMAIL]@l caicfycention.nhnC MayTeamTrialSizeSycepo9 ModolNotEoundEvcontior@ NoDoculteEycontion nhn@ NotimnlomantodEvcontin© NotSupportedException.g© TextRelayService.phg(©) ValidateSendingMessage.pnp© ConterencecrmmatcherJob.ong© OpportunityRepositorv.phgOpportunitySyncTrait.pho=custom.logscratch. &.ison= laravel.logA SF jiminny@localhost]& HS_local [jiminny@localhost]& console [PROD]A console (EU] X iii accounts [EU]fii stages (EU]tid teams [EU]ImporbotkecoraingJoo.org© Activity.php© FixActivitiesOpportunity.php© Opportunity.php& console SlAGiNGTy: AutoyPlavaroundySo jiminnym 030 A9 A27 X3 X106^.SELE * FROM users WhERE name LIKE "%rell Hovles': # 1651SELECT * FROM social accounts WHERE sociable id = 17651:116431| x16461645 v1646SELEC * FROM activities WERE uuid to bin('975c6830-7049-4cle-b2e9-ac80c10a738a') = uuid:=1647SELEC * FROM opportunities WHEREid IN (7842553, 6211727):SELECT * FROM contacts WHERE id IN (10202724, 6211727):SELE * FROM opportunity stages WHERE opportunity 1d = 7842553°SSLSMCONCAT(u.id, CASE WHEN U.id = t.owner_id THEN ' (owner)' ELSE'' END) AS user_idv.emailt.owner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_idWHERE u.team_id = 519 and sa.provider = 'hubspot';select * from crm confiqurations where id = 436:select * from crm profiles where crm confiquration id = 436: # 76091797 -> 16612select * from contact roles where contact id = 10202724:select * trom stages whereteam_id = 519: # 187781O SSELECTcrm provider 1d.stage 1d.is closed.is won.stage undated at.undated atFROM opportunitiesWHERE id TN 6211727. 7842553)4CELSCT * SP0M onnontunity contacteWHEPE onnontunitv id = 4211727 AN0 contact id - 1920272/•1678SELECT id, name, stage id, is closed, is won, updated at, remotely created atSP0M onnontuni+joc1682WHERE account id = 8179134npnEp Ry undatod at necr.• suppont Dally • In 10m100% L2. Thu 14 May 14:42:49AskJiminnyReportActivityServiceTest vcascadePipedrive SDK EvaluaFixing Activity Stage Dare you sure about that? Check https://github.com/pipedrive/client-php/blob/master/CHANGELOG.mdThoughtsCreated outline = raw.githubusercontent.comThought for 1sYou're absolutelv riaht - I was incorrect. Let me correct the information:Corrected SDK Last Update Datespipedrive/client-pho (Officia):Latest Version: 15.4.0• Last Updated: February 16, 2026•Link: https:/github.com/pipedrive/client-php/lolob/master/changeLog.mddeviolpipedrive (IsraelOrtuno:• Latest Version: 2.17.0•Last Updated: November 10, 2024Link: httos://aithub.com/israelOrtuno/oioedrive/releases/taa/2.17.0Undated RecommendationiThe ofticial SDK is signiticantly more recent (Feb 2026 vs Nov 2024) and actively maintained with trequent releases. ThisOtticial SDK Advantages NoW:•Much more recent updates (3 months ago vs 14 months ago)Very active develooment (15+ maior versions since May 2024)•Official Pipedrive support• API v2 support (critical for future-proofing)• OpenAPI-generated (automatic APl updates)• SIl more comolex AP desianNo Laravel intearation•Migration complexity remains hichThe official SDK's recent activity and API v2 support make it more compelling, but the migration effort and lack of Laraveintearation are still sianiticant tactorsYour included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Ask anvthina (84-L1« Code SWF-1.6W Windsurf Teamc1-1UTE.8io 4 spaces...
|
42573
|
NULL
|
NULL
|
NULL
|
|
45424
|
1626
|
34
|
2026-05-14T14:36:07.867668+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769367867_m2.jpg...
|
PhpStorm
|
faVsco.js – Playbook.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
Jiminny... ~ActivityFiles Project: faVsco.js, menu
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.38Yesterday at 13:37Yesterday at 13.30Yesterdav at 13:36resteraay at 15.3oYesterdav at 13:34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday at 15.3Yesterdav at 13:31Yesterday at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29resterday at 13.2oYesterdav at 13:27Yesterday at 13:27Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterdav at 13:23Yesterday at 13:22Yesterday at 13.20Yecterdav at 12:10Yesterdav at 13:19Yesterday at 13.10Yesterdav at 13:18Yesterday at 13:17Yesterday at 13:16Yacterdav at 12116Yesterdav at 13:15Yesterday at 13.1Yecterdav at 12:14Yesterday at 13:14Yesterday at 13:13Yoctorday at 12:19Yesterdav at 13:12Yesterday at 13.11Yesterdav at 13:10Yesterdav at 13:09v Size88 K:MPEG-4 movie62 KB71 KB23 Kb9 KE13 KB16 KB17 K:MPEG-4 movieMPEG-4 movieMPEG-4 movieMPE0"4 movie29 KB MPEG-4 movie6 KB12KBMP2G-4 movie9 KBI7 KBOKbMPEG-A movieMPEG"4 movie37 KE10 KBMoeeh movio7 KB8 K:MPEG-4 movieO KRI8 KB72 KBMPEG-1 movieMPEG-4 movie14 KEMPEG-4 movie13 KB9 KBMPEG-4 movie18 KB|MPEG-4 movie12 KPMDEG.A movic16 KBMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie12 KBMPEG-4 movie23 K:MPEG-4 movieMDEC A movid6KBMPEG-4 movie11 KPMDEG-A movie11 KBMPEG-4 movie20 KB34 KB10 KB7 KB5 KBMPEG-4 movieMDEC.A movialMPEG-4 movie11 KPMDEG-A movia26 KB MPEG-4 movie111 KBMPEG-4 movie102 KВMPSG-A movie88 KB MPEG-4 movie59 KB98 KB07 KpMPEG-4 movieMDEG.A movio66 KB44 KBMPEG-4 movie03 KрMDEG-A movid78 KBMPEG-4 movie50 KB58 K8MPEG-4 movieAl Notes: OffLeaveFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc toldeLocations0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenort 2.csviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence bettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.47 GB availabld• Inu 14 May 1/.30.01Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45421
|
NULL
|
NULL
|
NULL
|
|
45423
|
1625
|
56
|
2026-05-14T14:36:08.175661+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769368175_m1.jpg...
|
PhpStorm
|
faVsco.js – Playbook.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestv8• Thu 14 May 17:36:08* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php> Jobs.PlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php© DataClient.phpC Playbook.php X© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.php© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging18© LocalSearch.php© LocalSearchinterface.phpE.env© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© RemoteSearch.php© Service.php© HubspotPaginationService.phpclass Playbook extends ModelHandleHubspotRateLimit.phpv D Listeners© ConvertLeadActivities.php© PurgeLookupCache.php› Metadata> C Migrationv [ Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php© PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> • Fields> OpportunityMatcherOpportunitySyncStrategy> D ProspectSearchStrategyv • ServiceTraits A6public function getGroups(): Eloquent \Collection{...}public function hasTeam(): boolf...}public function getName(: string{...}public function getId(): int{...}public function getActivityType(): string{return $this->getAttribute( key: 'activity_type');public function getActivityField(): ?Field{...}public function getUuid(): string{...}public function getCategories(): Eloquent\Collection{...}T BatchSyncTrait.php( FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.pT SyncFieldsTrait.php@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v4143 ×4 л215|•m_layout_id =2162,1661,66799,66E217=218219-id = 33;— 220221222=223224id THEN •(own225226227228229230hubspot':-231232=233 V= 11512582;— 234|W Windsurf Teams182:21UTF-8Ca 4 spaces...
|
NULL
|
-225936720231251789
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestv8• Thu 14 May 17:36:08* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php> Jobs.PlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> O ProspectSearchStrategy> ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php© DataClient.phpC Playbook.php X© Pipedrive/Service.phpClose/Service.phpCopper/Service.php© BullhornService.php© DecorateActivity.php© PlainTextDecorateActivity.phpT ActivityPlaybookTrait.phpCrmHelperRepository.phpAccountController.php=.env.staging18© LocalSearch.php© LocalSearchinterface.phpE.env© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© RemoteSearch.php© Service.php© HubspotPaginationService.phpclass Playbook extends ModelHandleHubspotRateLimit.phpv D Listeners© ConvertLeadActivities.php© PurgeLookupCache.php› Metadata> C Migrationv [ Pipedrive> D OpportunitySyncStrategy> D ProspectSearchStrategy© ApiFields.php© Client.php© FieldDefinitions.php© PipedriveApiClient.php© PipedriveApiException.php© Service.php© TokenStorage.phpv D Salesforce> • Fields> OpportunityMatcherOpportunitySyncStrategy> D ProspectSearchStrategyv • ServiceTraits A6public function getGroups(): Eloquent \Collection{...}public function hasTeam(): boolf...}public function getName(: string{...}public function getId(): int{...}public function getActivityType(): string{return $this->getAttribute( key: 'activity_type');public function getActivityField(): ?Field{...}public function getUuid(): string{...}public function getCategories(): Eloquent\Collection{...}T BatchSyncTrait.php( FollowupActivityTrait.phpT LogActivityTrait.php+ RecordManipulationsTrait.pT SyncFieldsTrait.php@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v4143 ×4 л215|•m_layout_id =2162,1661,66799,66E217=218219-id = 33;— 220221222=223224id THEN •(own225226227228229230hubspot':-231232=233 V= 11512582;— 234|W Windsurf Teams182:21UTF-8Ca 4 spaces...
|
45422
|
NULL
|
NULL
|
NULL
|
|
45421
|
1626
|
33
|
2026-05-14T14:36:06.630276+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769366630_m2.jpg...
|
PhpStorm
|
faVsco.js – Playbook.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Jiminny\Component\Eloquent\Builder;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Layout;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Jiminny\Models\Playbook
*
* @property int $id
* @property mixed $uuid
* @property int $team_id
* @property string $activity_type
* @property int|null $activity_field_id
* @property string $name
* @property bool $is_selectable
* @property bool $ai_activity_type_detection_enabled
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read Field|null $activityField
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\PlaybookCategory> $categories
* @property-read int|null $categories_count
* @property-read string $id_string
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Group> $groups
* @property-read int|null $groups_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Layout> $layouts
* @property-read int|null $layouts_count
* @property-read \Jiminny\Models\Team $team
* @property-read int|null $templates_count
*
* @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\PlaybookFactory factory(...$parameters)
* @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)
* @method static Builder|Playbook newModelQuery()
* @method static Builder|Playbook newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook onlyTrashed()
* @method static Builder|Playbook query()
* @method static Builder|Playbook selectable($is_selectable = true)
* @method static Builder|Playbook uuid(string $uuid, bool $first = true)
* @method static Builder|Playbook whereActivityFieldId($value)
* @method static Builder|Playbook whereActivityType($value)
* @method static Builder|Playbook whereCreatedAt($value)
* @method static Builder|Playbook whereDeletedAt($value)
* @method static Builder|Playbook whereId($value)
* @method static Builder|Playbook whereIsSelectable($value)
* @method static Builder|Playbook whereName($value)
* @method static Builder|Playbook whereTeamId($value)
* @method static Builder|Playbook whereUpdatedAt($value)
* @method static Builder|Playbook whereUuid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withoutTrashed()
*
* @mixin \Eloquent
*/
class Playbook extends Model
{
use HasFactory;
use RequiresUUID;
use Enums;
use SoftDeletes;
public const string ACTIVITY_TYPE_TASK = 'task';
public const string ACTIVITY_TYPE_EVENT = 'event';
public static array $enumActivityTypes = [
self::ACTIVITY_TYPE_TASK,
self::ACTIVITY_TYPE_EVENT,
];
protected $table = 'playbooks';
protected $fillable = [
'name',
'team_id',
'activity_type',
'activity_field_id',
'is_selectable',
'ai_activity_type_detection_enabled',
];
protected $casts = [
'ai_activity_type_detection_enabled' => 'boolean',
];
protected $appends = [
'id_string',
];
protected $hidden = [
'uuid',
'is_selectable',
'id',
'team_id',
];
protected function casts(): array
{
return [
'is_selectable' => 'boolean',
];
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function layouts(): BelongsToMany
{
return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();
}
public function categories(): HasMany
{
return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');
}
public function groups(): HasMany
{
return $this->hasMany(Group::class)->orderBy('name', 'asc');
}
public function activityField(): BelongsTo
{
return $this->belongsTo(Field::class);
}
#[Scope]
protected function selectable($query, $isSelectable = true)
{
return $query->where('is_selectable', $isSelectable);
}
public function isSelectable(): bool
{
return $this->getAttribute('is_selectable');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
public function getGroups(): Eloquent\Collection
{
return $this->getAttribute('groups');
}
public function hasTeam(): bool
{
return $this->getAttribute('team') !== null;
}
public function getName(): string
{
return $this->getAttribute('name');
}
public function getId(): int
{
return $this->getAttribute('id');
}
public function getActivityType(): string
{
return $this->getAttribute('activity_type');
}
public function getActivityField(): ?Field
{
/**
* @var Field
*/
return $this->getAttribute('activityField');
}
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
public function getCategories(): Eloquent\Collection
{
return $this->getAttribute('categories');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Jiminny\\Component\\Eloquent\\Builder;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\n/**\n * Jiminny\\Models\\Playbook\n *\n * @property int $id\n * @property mixed $uuid\n * @property int $team_id\n * @property string $activity_type\n * @property int|null $activity_field_id\n * @property string $name\n * @property bool $is_selectable\n * @property bool $ai_activity_type_detection_enabled\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property-read Field|null $activityField\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\PlaybookCategory> $categories\n * @property-read int|null $categories_count\n * @property-read string $id_string\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Group> $groups\n * @property-read int|null $groups_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Layout> $layouts\n * @property-read int|null $layouts_count\n * @property-read \\Jiminny\\Models\\Team $team\n * @property-read int|null $templates_count\n *\n * @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\PlaybookFactory factory(...$parameters)\n * @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)\n * @method static Builder|Playbook newModelQuery()\n * @method static Builder|Playbook newQuery()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook onlyTrashed()\n * @method static Builder|Playbook query()\n * @method static Builder|Playbook selectable($is_selectable = true)\n * @method static Builder|Playbook uuid(string $uuid, bool $first = true)\n * @method static Builder|Playbook whereActivityFieldId($value)\n * @method static Builder|Playbook whereActivityType($value)\n * @method static Builder|Playbook whereCreatedAt($value)\n * @method static Builder|Playbook whereDeletedAt($value)\n * @method static Builder|Playbook whereId($value)\n * @method static Builder|Playbook whereIsSelectable($value)\n * @method static Builder|Playbook whereName($value)\n * @method static Builder|Playbook whereTeamId($value)\n * @method static Builder|Playbook whereUpdatedAt($value)\n * @method static Builder|Playbook whereUuid($value)\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withTrashed()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Playbook extends Model\n{\n use HasFactory;\n\n use RequiresUUID;\n use Enums;\n use SoftDeletes;\n\n public const string ACTIVITY_TYPE_TASK = 'task';\n public const string ACTIVITY_TYPE_EVENT = 'event';\n\n public static array $enumActivityTypes = [\n self::ACTIVITY_TYPE_TASK,\n self::ACTIVITY_TYPE_EVENT,\n ];\n\n protected $table = 'playbooks';\n\n protected $fillable = [\n 'name',\n 'team_id',\n 'activity_type',\n 'activity_field_id',\n 'is_selectable',\n 'ai_activity_type_detection_enabled',\n ];\n\n protected $casts = [\n 'ai_activity_type_detection_enabled' => 'boolean',\n ];\n\n protected $appends = [\n 'id_string',\n ];\n\n protected $hidden = [\n 'uuid',\n 'is_selectable',\n 'id',\n 'team_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'is_selectable' => 'boolean',\n ];\n }\n\n public function team(): BelongsTo\n {\n return $this->belongsTo(Team::class);\n }\n\n public function layouts(): BelongsToMany\n {\n return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();\n }\n\n public function categories(): HasMany\n {\n return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');\n }\n\n public function groups(): HasMany\n {\n return $this->hasMany(Group::class)->orderBy('name', 'asc');\n }\n\n public function activityField(): BelongsTo\n {\n return $this->belongsTo(Field::class);\n }\n\n #[Scope]\n protected function selectable($query, $isSelectable = true)\n {\n return $query->where('is_selectable', $isSelectable);\n }\n\n public function isSelectable(): bool\n {\n return $this->getAttribute('is_selectable');\n }\n\n public function getTeam(): Team\n {\n return $this->getAttribute('team');\n }\n\n public function getTeamId(): int\n {\n return $this->getAttribute('team_id');\n }\n\n public function getGroups(): Eloquent\\Collection\n {\n return $this->getAttribute('groups');\n }\n\n public function hasTeam(): bool\n {\n return $this->getAttribute('team') !== null;\n }\n\n public function getName(): string\n {\n return $this->getAttribute('name');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getActivityType(): string\n {\n return $this->getAttribute('activity_type');\n }\n\n public function getActivityField(): ?Field\n {\n /**\n * @var Field\n */\n return $this->getAttribute('activityField');\n }\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getCategories(): Eloquent\\Collection\n {\n return $this->getAttribute('categories');\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Jiminny\\Component\\Eloquent\\Builder;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\n/**\n * Jiminny\\Models\\Playbook\n *\n * @property int $id\n * @property mixed $uuid\n * @property int $team_id\n * @property string $activity_type\n * @property int|null $activity_field_id\n * @property string $name\n * @property bool $is_selectable\n * @property bool $ai_activity_type_detection_enabled\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property-read Field|null $activityField\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\PlaybookCategory> $categories\n * @property-read int|null $categories_count\n * @property-read string $id_string\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Group> $groups\n * @property-read int|null $groups_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Layout> $layouts\n * @property-read int|null $layouts_count\n * @property-read \\Jiminny\\Models\\Team $team\n * @property-read int|null $templates_count\n *\n * @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\PlaybookFactory factory(...$parameters)\n * @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)\n * @method static Builder|Playbook newModelQuery()\n * @method static Builder|Playbook newQuery()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook onlyTrashed()\n * @method static Builder|Playbook query()\n * @method static Builder|Playbook selectable($is_selectable = true)\n * @method static Builder|Playbook uuid(string $uuid, bool $first = true)\n * @method static Builder|Playbook whereActivityFieldId($value)\n * @method static Builder|Playbook whereActivityType($value)\n * @method static Builder|Playbook whereCreatedAt($value)\n * @method static Builder|Playbook whereDeletedAt($value)\n * @method static Builder|Playbook whereId($value)\n * @method static Builder|Playbook whereIsSelectable($value)\n * @method static Builder|Playbook whereName($value)\n * @method static Builder|Playbook whereTeamId($value)\n * @method static Builder|Playbook whereUpdatedAt($value)\n * @method static Builder|Playbook whereUuid($value)\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withTrashed()\n * @method static \\Illuminate\\Database\\Eloquent\\Builder|Playbook withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Playbook extends Model\n{\n use HasFactory;\n\n use RequiresUUID;\n use Enums;\n use SoftDeletes;\n\n public const string ACTIVITY_TYPE_TASK = 'task';\n public const string ACTIVITY_TYPE_EVENT = 'event';\n\n public static array $enumActivityTypes = [\n self::ACTIVITY_TYPE_TASK,\n self::ACTIVITY_TYPE_EVENT,\n ];\n\n protected $table = 'playbooks';\n\n protected $fillable = [\n 'name',\n 'team_id',\n 'activity_type',\n 'activity_field_id',\n 'is_selectable',\n 'ai_activity_type_detection_enabled',\n ];\n\n protected $casts = [\n 'ai_activity_type_detection_enabled' => 'boolean',\n ];\n\n protected $appends = [\n 'id_string',\n ];\n\n protected $hidden = [\n 'uuid',\n 'is_selectable',\n 'id',\n 'team_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'is_selectable' => 'boolean',\n ];\n }\n\n public function team(): BelongsTo\n {\n return $this->belongsTo(Team::class);\n }\n\n public function layouts(): BelongsToMany\n {\n return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();\n }\n\n public function categories(): HasMany\n {\n return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');\n }\n\n public function groups(): HasMany\n {\n return $this->hasMany(Group::class)->orderBy('name', 'asc');\n }\n\n public function activityField(): BelongsTo\n {\n return $this->belongsTo(Field::class);\n }\n\n #[Scope]\n protected function selectable($query, $isSelectable = true)\n {\n return $query->where('is_selectable', $isSelectable);\n }\n\n public function isSelectable(): bool\n {\n return $this->getAttribute('is_selectable');\n }\n\n public function getTeam(): Team\n {\n return $this->getAttribute('team');\n }\n\n public function getTeamId(): int\n {\n return $this->getAttribute('team_id');\n }\n\n public function getGroups(): Eloquent\\Collection\n {\n return $this->getAttribute('groups');\n }\n\n public function hasTeam(): bool\n {\n return $this->getAttribute('team') !== null;\n }\n\n public function getName(): string\n {\n return $this->getAttribute('name');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getActivityType(): string\n {\n return $this->getAttribute('activity_type');\n }\n\n public function getActivityField(): ?Field\n {\n /**\n * @var Field\n */\n return $this->getAttribute('activityField');\n }\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getCategories(): Eloquent\\Collection\n {\n return $this->getAttribute('categories');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.28224733,"top":1.0,"width":0.024268618,"height":-0.04788506},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-161723684282621552
|
4668716167454726109
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
16
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Jiminny\Component\Eloquent\Builder;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Layout;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Jiminny\Models\Playbook
*
* @property int $id
* @property mixed $uuid
* @property int $team_id
* @property string $activity_type
* @property int|null $activity_field_id
* @property string $name
* @property bool $is_selectable
* @property bool $ai_activity_type_detection_enabled
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read Field|null $activityField
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\PlaybookCategory> $categories
* @property-read int|null $categories_count
* @property-read string $id_string
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Group> $groups
* @property-read int|null $groups_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Layout> $layouts
* @property-read int|null $layouts_count
* @property-read \Jiminny\Models\Team $team
* @property-read int|null $templates_count
*
* @method static Builder|Playbook chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\PlaybookFactory factory(...$parameters)
* @method static Builder|Playbook idOrUuId($idOrUuid, bool $first = true)
* @method static Builder|Playbook newModelQuery()
* @method static Builder|Playbook newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook onlyTrashed()
* @method static Builder|Playbook query()
* @method static Builder|Playbook selectable($is_selectable = true)
* @method static Builder|Playbook uuid(string $uuid, bool $first = true)
* @method static Builder|Playbook whereActivityFieldId($value)
* @method static Builder|Playbook whereActivityType($value)
* @method static Builder|Playbook whereCreatedAt($value)
* @method static Builder|Playbook whereDeletedAt($value)
* @method static Builder|Playbook whereId($value)
* @method static Builder|Playbook whereIsSelectable($value)
* @method static Builder|Playbook whereName($value)
* @method static Builder|Playbook whereTeamId($value)
* @method static Builder|Playbook whereUpdatedAt($value)
* @method static Builder|Playbook whereUuid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Playbook withoutTrashed()
*
* @mixin \Eloquent
*/
class Playbook extends Model
{
use HasFactory;
use RequiresUUID;
use Enums;
use SoftDeletes;
public const string ACTIVITY_TYPE_TASK = 'task';
public const string ACTIVITY_TYPE_EVENT = 'event';
public static array $enumActivityTypes = [
self::ACTIVITY_TYPE_TASK,
self::ACTIVITY_TYPE_EVENT,
];
protected $table = 'playbooks';
protected $fillable = [
'name',
'team_id',
'activity_type',
'activity_field_id',
'is_selectable',
'ai_activity_type_detection_enabled',
];
protected $casts = [
'ai_activity_type_detection_enabled' => 'boolean',
];
protected $appends = [
'id_string',
];
protected $hidden = [
'uuid',
'is_selectable',
'id',
'team_id',
];
protected function casts(): array
{
return [
'is_selectable' => 'boolean',
];
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function layouts(): BelongsToMany
{
return $this->belongsToMany(Layout::class, 'playbook_layouts')->withTimestamps();
}
public function categories(): HasMany
{
return $this->hasMany(PlaybookCategory::class)->orderBy('sequence', 'asc');
}
public function groups(): HasMany
{
return $this->hasMany(Group::class)->orderBy('name', 'asc');
}
public function activityField(): BelongsTo
{
return $this->belongsTo(Field::class);
}
#[Scope]
protected function selectable($query, $isSelectable = true)
{
return $query->where('is_selectable', $isSelectable);
}
public function isSelectable(): bool
{
return $this->getAttribute('is_selectable');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
public function getGroups(): Eloquent\Collection
{
return $this->getAttribute('groups');
}
public function hasTeam(): bool
{
return $this->getAttribute('team') !== null;
}
public function getName(): string
{
return $this->getAttribute('name');
}
public function getId(): int
{
return $this->getAttribute('id');
}
public function getActivityType(): string
{
return $this->getAttribute('activity_type');
}
public function getActivityField(): ?Field
{
/**
* @var Field
*/
return $this->getAttribute('activityField');
}
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
public function getCategories(): Eloquent\Collection
{
return $this->getAttribute('categories');
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22822
|
976
|
50
|
2026-05-12T07:22:11.330522+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570531330_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1590047453762137304
|
-4275742795314862131
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
22821
|
NULL
|
NULL
|
NULL
|
|
22821
|
976
|
49
|
2026-05-12T07:22:09.717779+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570529717_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2357695412272837511
|
-7592713776232584246
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
QuickTime PlayerFileEditViewWindowHelpPetko KashinskiScreen shareChromeFileEditViewHistoryBookmarksProfiles•••Greet@ Scorerun.userpilot.io/dashboards/product-usageС АIKВ• ChatPlayground Al...1 Jiminny - Calenda….M GMail$ 0(ahl• Support Daily - in 4 h 38 m100% <78• Tue 12 May 10:22:09TabWindowHelp@ andre• wilso*QCall AJiminM Inbox= Nate = AppsBuild u User;© New+1%8• Mon 11 May 12:18I L x+1© Work• My Calendly - Eve....= PH New Ul LoginGGet Starting with J...*Apps|Chloe Onboarding...# CX Journey SM....62 Huddle with Lukas Kovalik?=Al Notes: Off&+ГALeave...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22820
|
977
|
43
|
2026-05-12T07:22:09.715474+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570529715_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.39162233,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.40159574,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4112367,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.41855052,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73105055,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.44481382,"top":0.09736632,"width":0.55518615,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.44481382,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.44481382,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.44481382,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.44481382,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.44481382,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.44481382,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.44481382,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.44481382,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.44481382,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.44481382,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.44481382,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.44481382,"top":0.096568234,"width":0.55518615,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.44481382,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.44481382,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.44481382,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.44481382,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.44481382,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.44481382,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.44481382,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.44481382,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.44481382,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.44481382,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.44481382,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.44481382,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1590047453762137304
|
-4275742795314862131
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
22819
|
NULL
|
NULL
|
NULL
|
|
22819
|
977
|
42
|
2026-05-12T07:22:07.224347+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570527224_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-288897650834379520
|
-7591587877936354358
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
PhostormVIewINavicarecodeWindowrTavsco.s?9 JY-20725-harroledeyC) AutomatedReportGenerated.onp© PlaybackController.php xA SF (jiminny@localhost]4 HS_local jiminny@localhost]# console [PKol)& console [EU]C) SubscrintionControfinal class PlaybackController extends FrontendControllerВÔMЗАУA console [STAGING]C) TeamA AutomationC) TeamA ConteytConФ TeamController.phpc) ToaminciahtcContro transcrlpuonconu© TranslationControll© UserController.php© VocabularyControll>@ Auth-customerapl›J Internav D Kiosk• eams© ActivityController.pAutomatedReportsic) DashboardControllec) ImpersonationContc) Orcanizationscontr% PartnersController.tC) ProfileController.ohC)SearchController.of>• SettingsM Telenhonvv M Wehhook> • Hubspot>D IntegrationAppSub:© ActivityProviderCor© ActivityTranscriptio© BaseController.phpCn CnlondarControllor© ReportController.phSoftphoneWebhoolAostracicontroller.onc@ CommentContextinterc) conterencesOptinOutc) controller.phpExportController.phpTFrontendcontroller ira@ GeocodinaController.n(C) HealthCheckControllei@ LiveCoachController.p(C) Missina TeamControlleC) MobileController.ohoYe) PlavbackControlier ohg DlavlistController nhnpublic tunction show Activity sactivity, Playbackragelranstormer ecranstormer, kequest grequest). arrayistring[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {Scata = Sractall: crenteoi>itemdSactivity,Stransformen->setConsumer(Suser)-senializeWithinew IsonSenializeron->соАrrayи$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);if (! isset($data['playbackData']['tracks'])) {$data['playbackData']['tracks'] = []:* Sending 'plaubackVisited' event to Planhat without slowing the* response to the user e.a.orter the response 1s sent back.deferfn ( => Sthis->planhatService->track(navload"'activityid' => $activity->getid,'activitvluid' = Sactivitv->oetluido→->alwavs O•notunn Sthic-snendendlInlavhackhatal =s?'activity' => $data('playbackData']'favorited' => $data['favorited'].'subscribed' => $data['subscribed'].'view' => $viewprivate function getPreloadedPlavlist(Activity Sactivity): arrav{...}* othrows AuthorizationExcention(C) PucherController nhng SlackController nhn146|05>GET /olavlist/activitv1.m3u8 lolavlistlnublic function plavlist(Activity Sactivitv): Response!...?"Vace".L"Inu,or May 2020 14.21.15 bMl"J,"Transter-Encodinq":"chunked")."CF-Ray": ["9f80deb8db60dc3a-SOF"]."Strict-Transport-Security":["max-aqe=31536000: includeSubDomains: preload"]."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\","x-content-tvoe-ontions": "nosniff")."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7."Set-Cookie":["__cf_bm=StUrtdQgXVnik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"),"Renont-To".f"s"endnoints".'\"url\":"https:\V/\V/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIx\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,report to. "cr-nel,|"max agel ":604800}"]"Server": ["cloudflare"]}} {"correlation_1d": "95256555-eC98-4541-D92a-adta/Sb6veab","trace 10":"C/AD8565-905t-4604-9405-8e5b551e5545>0 lll 0supoont Dally • In 41 30m100% 5• lue 1z May 10-22*00cascadePlanhat Event Playbac+0 ..find planhat event playback visitedThoughtsSearched planhat in ~/iminnylappThoughtsSearched olavback *visitedivisited."olavback in ~/fiminnvlaoolSoarched pl.Searched visited in aprThouahtslFound it in PlaybackController.php :Read DlavbackController.nhn #| 85-114Location:Plavbackcontroller.0no:95-168oho.*Cendina InlavbackVicitedl event to Planhat without clowina +hedeferlfn = Sthis->olanhatService-strack(user. Suserpay load:'activityld' = Sactivity->getId(),activitvluid' = sactivity-saetluido.-salwavs@)Contovt. Collod in tha niavhaak nado controllor wronnod in doforl to firo tha Dlanhat avant scunchronduely aftor thoHTTP response is sent to avoid slowina down the user experience.Oal *Jed daily usage quota is exhausted. Purchase extra usage to continue usina premium models. Quota resets May 12, 11:00"AN CUTYAsk anvthina (84L)÷ @Code SWF-1.6105•60JUTE.Afo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22818
|
976
|
48
|
2026-05-12T07:22:08.274172+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570528274_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1555762331299765567
|
-4274511341738104884
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification...
|
22817
|
NULL
|
NULL
|
NULL
|
|
22817
|
976
|
47
|
2026-05-12T07:22:07.202552+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570527202_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-288897650834379520
|
-7591587877936354358
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
QuickTime PlayerFileEditViewWindowHelpPetko KashinskiScreen shareChromeFileEditViewHistoryBookmarksProfiles.. .GreetScoreCС АIKВ• ChatPlayground Al...10 Jiminny - Calenda…M GMailj Support Daily - in 4h 38 mTabWindowHelp@ andre@ wilsoCall AJiminM InboxNate• My Calendly - Eve....= PH New Ul LoginGet Starting with J...GoogleSearch Google or type a URLPhpStormAdd shortcut62 Huddle with Lukas KovalikAl Notes: OffGoogle= AppsBuildu User;© NewC Al ModeAppsChloe Onboarding....+ CX Journey SMB...•JiminnySalesforceUserpilotLggin| MaxioHome | HookReachdeskIntercomDashboard | GetAcceptEfficient contract management for modern businessesJira*Jira ConfluenceN• CloudAppHubSpotBambooHRThe LoopGoogle DriveGoogle groups docsPublic Profile - ConveyorSign up | Miro | The Visual Workspace for InnovationVision by The OrgLoomActivity Feed | Crunchbase100% (8•Tue 12 May 10:22:06+1%8• Mon 11 May 12:185 л x+10 Workages31Sustomise Chrome&ГАLeave...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22633
|
974
|
24
|
2026-05-12T07:16:21.018616+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570181018_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1590047453762137304
|
-4275742795314862131
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
22631
|
NULL
|
NULL
|
NULL
|
|
22632
|
975
|
27
|
2026-05-12T07:16:15.880671+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570175880_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.39162233,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.40159574,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4112367,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.41855052,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73105055,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.44481382,"top":0.09736632,"width":0.55518615,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.44481382,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.44481382,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.44481382,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.44481382,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.44481382,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.44481382,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.44481382,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.44481382,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.44481382,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.44481382,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.44481382,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.44481382,"top":0.096568234,"width":0.55518615,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.44481382,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.44481382,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.44481382,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.44481382,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.44481382,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.44481382,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.44481382,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.44481382,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.44481382,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.44481382,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.44481382,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.44481382,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1590047453762137304
|
-4275742795314862131
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22631
|
974
|
23
|
2026-05-12T07:16:15.842838+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570175842_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1665678769239655029
|
-4275707610942511156
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22621
|
975
|
19
|
2026-05-12T07:15:37.710929+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570137710_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.39162233,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.40159574,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4112367,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.41855052,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73105055,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.44481382,"top":0.09736632,"width":0.55518615,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.44481382,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.44481382,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.44481382,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.44481382,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.44481382,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.44481382,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.44481382,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.44481382,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.44481382,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.44481382,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.44481382,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.44481382,"top":0.096568234,"width":0.55518615,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.44481382,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.44481382,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.44481382,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.44481382,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.44481382,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.44481382,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.44481382,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.44481382,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.44481382,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.44481382,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.44481382,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.44481382,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1590047453762137304
|
-4275742795314862131
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22620
|
974
|
20
|
2026-05-12T07:15:37.703833+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570137703_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2364246763852313945
|
-4562741786609293364
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes...
|
22619
|
NULL
|
NULL
|
NULL
|
|
22619
|
974
|
19
|
2026-05-12T07:15:32.626478+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570132626_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1590047453762137304
|
-4275742795314862131
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22618
|
974
|
18
|
2026-05-12T07:15:29.556802+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570129556_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-515286014873768794
|
-4276763141551790132
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide...
|
22616
|
NULL
|
NULL
|
NULL
|
|
22617
|
975
|
18
|
2026-05-12T07:15:28.206186+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570128206_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.39162233,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.40159574,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4112367,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.41855052,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73105055,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.44481382,"top":0.09736632,"width":0.55518615,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.44481382,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.44481382,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.44481382,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.44481382,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.44481382,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.44481382,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.44481382,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.44481382,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.44481382,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.44481382,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.44481382,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.44481382,"top":0.096568234,"width":0.55518615,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.44481382,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.44481382,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.44481382,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.44481382,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.44481382,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.44481382,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.44481382,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.44481382,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.44481382,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.44481382,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.44481382,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.44481382,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6580691120320665657
|
-4259980196619032627
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…...
|
22613
|
NULL
|
NULL
|
NULL
|
|
22616
|
974
|
17
|
2026-05-12T07:15:28.206170+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570128206_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1555762331299765567
|
-4274511341738104884
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22615
|
974
|
16
|
2026-05-12T07:15:26.551382+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570126551_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1590047453762137304
|
-4275742795314862131
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
22614
|
NULL
|
NULL
|
NULL
|
|
22614
|
974
|
15
|
2026-05-12T07:15:25.719046+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570125719_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1579713784452094674
|
-8780890023316608054
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
QuickTime PlayerFileEditViewWindowHelp(aholSupport Daily - in 4h 45 mPetko KashinskiScreen snare• PLanhat Petko interest event 2026-05-11.mp4|SlackFileEditViewGoHistoryWindowHelpQWorlGreetiScorecandrejf wilsonCall ArJiminnM Inbox=Nate R= AFBuildiru Userpws.planhat.com/jiminny/apps?id=66ceb97643c2530bb32c8bb6• AIKB• ChatPlayground Ali....Jiminny - Calenda...M GMailMy Calendly - Eve….= PH New UI LoginGet Starting with J....C AppsChloe Onboarding-+ CX Journey SMB.+ BackSearch Jiminny83 App Center& UP > PH UXE All apps& Created by meG Recentty most activeIntegrations8 AutomationsP Private apps0 EditorPa Runs8 Data100% <78• Tue 12 May 10:15:25+X8• Mon 11 May 12:17Newt+f Work. Petko•* XEnabled4 Webhook incomingA WebhookWebhookEvent details8bcea7d0-160b- @TriggeranythingOmsTriggered by& Support User (Removed)6 Huddle with Lukas KovalikCa Event log9= Al Notes: OffallQ 100%00:07-I04:557Leav...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22613
|
975
|
17
|
2026-05-12T07:15:25.718957+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570125718_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.39162233,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.40159574,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4112367,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.41855052,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2364246763852313945
|
-4562741786609293364
|
app_switch
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22591
|
975
|
5
|
2026-05-12T07:14:44.938447+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570084938_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.39162233,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.40159574,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4112367,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.41855052,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73105055,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.44481382,"top":0.09736632,"width":0.55518615,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.44481382,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.44481382,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.44481382,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.44481382,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.44481382,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.44481382,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.44481382,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.44481382,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.44481382,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.44481382,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.44481382,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.44481382,"top":0.096568234,"width":0.55518615,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.44481382,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.44481382,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.44481382,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.44481382,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.44481382,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.44481382,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.44481382,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.44481382,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.44481382,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.44481382,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.44481382,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.44481382,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1665678769239655029
|
-4275707610942511156
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
22590
|
NULL
|
NULL
|
NULL
|
|
22581
|
NULL
|
0
|
2026-05-12T07:14:17.037705+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570057037_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.82413566,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-817699417600123759
|
-7159315621853853246
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
PhostormVIewINavicareCodeWindowFV faVsco.js°9 JY-20725-handle-HS-search-rate-limitProiectC) AutomatedReportGenerated.onp© PlaybackController.php xC) SubscrintionControfinal clacs PlavhackController extends FrontendcontroulerC) TeamA AutomationC) TeamAiConteytConlФ TeamController.phpc) ToaminciahtcContr© TranscriptionContrc© TranslationControll© UserController.php© VocabularyControll>@ Auth-customerapl›J Internav D Kioskeams© ActivityController.pAutomatedReportsic) DashboardControllec) ImpersonationContc) Orcanizationscontr0 PartnersController.C) ProfileController.ohC)SearchController.of>• SettingsM Telenhonvv M Wehhook>D Hubspot)M intearationAnnSub‹© ActivityProviderCor© ActivityTranscriptio© BaseController.php© CalendarController,© ReportController.phSoftphoneWebhoolAbstractController.onc@ CommentContextinterc) conterencesOptinOutc) controller.phpExportController.phpTFrontendcontroller ira@ GeocodinaController.n(C) HealthCheckControllei@ LiveCoachController.p(C) Missina TeamControlleC) MobileController.ohoYe) PlavbackControlier ohg DlavlistController nhnpudLlc Tunction snow Activlcy$data = Fractal::createQ->item(Saculvity.Stransformer->setConsumer(Suser)->serzal1zewichnew Jsonserzallzero->соАггаyОнsoacalplaybackvaca'"mascerPlayu1st = sch1s->getPreloadedrlayu1stsaccivicynif (! isset(Sdata['playbackData']['tracks'])) {Sdatal'playbackData']f'tracks'] = []:* Sendina 'plaubackVisited' event to Planhat without slowing the* response to the user e.a.deferdfnO => Sthis->nlanhatService->trackduser: Suserevent: 'playbackVisited',payload: ['activityId' => $activity->getId,'activityUuid' => Sactivity->getUuid.)->alwaysO:return Sthis->render(OpLaybackbara =>'activity' => Sdata['playbackData']'favorited' => $data[ 'favorited']'subscribed' => $data( 'subscribed'].orivate function detPreloadedPlavlistActivitv Sactivitv): arrav-...;* athrows AuthorizationExcentionGET Inlavlict[activitv) m2u8 fnlavlict1(C) PucherController nhng SlackController nhnuched 1 commit to oriain/IV.20725-handle.HS-coarch-rate-limit |/ View null reauect (vecterdav 10•021public function playlist(Activity $activity): Response{...}A SF (jiminny@localhost]4 HS_local [jiminny@localhost]# console [PRol)ВÔMЗАУA console [STAGING]PlaybackPageTransformer $transformer, Request Srequest): array|string[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Vace".L"Inu,or May 2020 14.21.15 bMl"J,"Transter-Encodinq":"chunked")."CF-Ray": ["9f80deb8db60dc3a-SOF"]."Strict-Transport-Security":["max-aqe=31536000: includeSubDomains: preload"].# console [eu)"server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3)","x-content-tvoe-ontions": "nosniff")."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"7."Set-Cookie":["__cf_bm=StUrtdQgXVnik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfW07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"),"Renont-To".f"s"endnoints".'\"url\":"https:\V/\V/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIx\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,report to. "cr-nel,|"max agel ":604800}"]"Server": ["cloudflare"]}} {"correlation 1d":"95256555-ec98-4541-b9za-adta/sboyeab"."trace 10":"C/AD8565-905t-4604-9405-8e5b551e5545"suppont Dally • In 4h 40 m100% 5• Tue 12 May 10:14:16HandleHubspotRateLimitTest vcascadePlanhat Event Playbac+0..find planhat event playback visitedThoughtsSearched planhat in ~/iminnylappThoughtsSearched olavback *visitedivisited."olavback in ~/fiminnvlaoolSearched olavback in anniSoarched DlaSearched visited in aprThouahtslFound it in PlaybackController.php :Read DlavbackController.nhn #| 85-114Location:Plavbackcontroller.0no:95-168oho.*Cendina InlavbackVicitedi event to Planhat without clowina thedeferlfn = Sthis->olanhatService-strack(user. Suserpayload: ['activityld' = Sactivity->getId(),activitvluid' = sactivity-aetluido.-salwavs@)Contovt. Collod in tha niavhaak nado controllor wronnod in doforl to firo tha Dlanhat avant scunchronduely aftor thoHTTP response is sent to avoid slowina down the user experience.Oal ***Jed daily usage quota is exhausted. Purchase extra usage to continue usina premium models. Quota resets May 12, 11:00"AN CUTYAsk anvthina (84L)÷ @Code SWF-1.6WN Windsurf Toams 102-12 UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22579
|
NULL
|
0
|
2026-05-12T07:14:16.212811+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570056212_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FinderFileEditViewGoWindowHelp(ah)Support Daily - FinderFileEditViewGoWindowHelp(ah)Support Daily - in 4h 46 mADOCKERO ₴1DEV (-zsh)O $2ggml_metal_init:allocatingggml_metal_init:found device: Apple M1ggml_metal_init:picking default device: Apple M1ggml_metal_init:use fusion= trueggml_metal_init:use concurrencytrueggml_metal_init:use graph optimize=truewhisper_backend_init: using BLAS backendwhisper_init_state: kv self size=3.15 MBwhisper_init_state: kv cross size =9.44 MBwhisper_init_state: kv padsize=2.36 MBwhisper_init_state:compute buffer (conv)whisper_init_state:compute buffer(encode)whisper_init_state:computebuffer (cross)whisper_init_state: computebuffer (decode)ggml_metal_free: deallocatingwhisper_backend_init_gpu: device 0:Metal(type:1)whiKAAlAnASлunАCDl1APP (-zsh)14.17 MB65.96 MB8.50 MB96.83 MBPS5$Ig9gwh.PhpStormwhisper_init_state:kvselfsize3.15MBwhisper_init_state: kvcrosssize=9.44 MBwhisper_init_state: kv padsize=2.36 MBwhisper_init_state:compute buffer (conv)whisper_init_state: computebuffer (encode)whisper_init_state:computebuffer (cross)=whisper_init_state:compute buffer (decode) =14.17 MB65.96 MB8.50 MB96.83 MBggml_metal_free:deallocatingwhispr-device 0: Metal (type:whiskfound GPU device 0: Metal (type: 1, cnt: 0)whis!using Metal backend9gmlggmlrice: Apple M1ggmldefault device: Apple M19gml4 27m 56s1,02 GBon= trueggml_metal_init:use concurrency= trueggml_metal_init: use graph optimize= truewhisper_backend_init: usingBLAS backendwhisper_init_state: kv self size3.15 MBwhisper_init_state: kv cross size9.44 MB883screenpipe"-zsh*4-zshX5screenpipe"786100% (8•Tue 12 May 10:14:15181-zsh87Copying "CleanShot 2026-0...at 09.45.47.mp4" to "2026"268,4 MB of 1,02 GB - Less than a minute...
|
NULL
|
8361796479739152917
|
NULL
|
visual_change
|
ocr
|
NULL
|
FinderFileEditViewGoWindowHelp(ah)Support Daily - FinderFileEditViewGoWindowHelp(ah)Support Daily - in 4h 46 mADOCKERO ₴1DEV (-zsh)O $2ggml_metal_init:allocatingggml_metal_init:found device: Apple M1ggml_metal_init:picking default device: Apple M1ggml_metal_init:use fusion= trueggml_metal_init:use concurrencytrueggml_metal_init:use graph optimize=truewhisper_backend_init: using BLAS backendwhisper_init_state: kv self size=3.15 MBwhisper_init_state: kv cross size =9.44 MBwhisper_init_state: kv padsize=2.36 MBwhisper_init_state:compute buffer (conv)whisper_init_state:compute buffer(encode)whisper_init_state:computebuffer (cross)whisper_init_state: computebuffer (decode)ggml_metal_free: deallocatingwhisper_backend_init_gpu: device 0:Metal(type:1)whiKAAlAnASлunАCDl1APP (-zsh)14.17 MB65.96 MB8.50 MB96.83 MBPS5$Ig9gwh.PhpStormwhisper_init_state:kvselfsize3.15MBwhisper_init_state: kvcrosssize=9.44 MBwhisper_init_state: kv padsize=2.36 MBwhisper_init_state:compute buffer (conv)whisper_init_state: computebuffer (encode)whisper_init_state:computebuffer (cross)=whisper_init_state:compute buffer (decode) =14.17 MB65.96 MB8.50 MB96.83 MBggml_metal_free:deallocatingwhispr-device 0: Metal (type:whiskfound GPU device 0: Metal (type: 1, cnt: 0)whis!using Metal backend9gmlggmlrice: Apple M1ggmldefault device: Apple M19gml4 27m 56s1,02 GBon= trueggml_metal_init:use concurrency= trueggml_metal_init: use graph optimize= truewhisper_backend_init: usingBLAS backendwhisper_init_state: kv self size3.15 MBwhisper_init_state: kv cross size9.44 MB883screenpipe"-zsh*4-zshX5screenpipe"786100% (8•Tue 12 May 10:14:15181-zsh87Copying "CleanShot 2026-0...at 09.45.47.mp4" to "2026"268,4 MB of 1,02 GB - Less than a minute...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16801
|
751
|
5
|
2026-05-11T09:28:26.037122+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778491706037_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.37699467,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.38696808,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39660904,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4039229,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42918882,"top":0.09736632,"width":0.57081115,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.42918882,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.42918882,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.42918882,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.42918882,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.42918882,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.42918882,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.42918882,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.42918882,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.42918882,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.42918882,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.42918882,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.42918882,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.42918882,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.42918882,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.42918882,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.42918882,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.42918882,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.42918882,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.42918882,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.42918882,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.42918882,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.42918882,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.42918882,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.42918882,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4283169546787192088
|
-4256602496898242612
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16800
|
750
|
4
|
2026-05-11T09:28:12.839584+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778491692839_m1.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3876230358461125011
|
-4259980196618770484
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16799
|
751
|
4
|
2026-05-11T09:28:12.541582+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778491692541_m2.jpg...
|
PhpStorm
|
faVsco.js – PlaybackController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.37699467,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.38696808,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39660904,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4039229,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Notifications\\DatabaseNotification;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\PlaybackPage\\Download\\Services\\DownloadActivityService;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\PlaybackPageTransformer;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Services\\PlanhatService;\nuse Jiminny\\Services\\PlaybackService;\nuse JsonException;\nuse Spatie\\Fractal\\Fractal;\nuse Illuminate\\Support\\Facades\\Cookie;\n\nfinal class PlaybackController extends FrontendController\n{\n use AuthorizesRequests;\n\n public function __construct(\n private readonly PlaybackService $playbackService,\n private readonly DownloadActivityService $downloadActivityService,\n private readonly PlanhatService $planhatService,\n ) {\n }\n\n /**\n * @throws AuthorizationException\n * @throws JsonException\n */\n public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string\n {\n $this->authorize('view', $activity);\n\n /** @var User $user */\n $user = $request->user();\n\n $activityTypeCheck = in_array(\n $activity->type,\n [\n Activity::TYPE_CONFERENCE,\n Activity::TYPE_SOFTPHONE,\n Activity::TYPE_SOFTPHONE_INBOUND,\n ],\n true\n );\n\n abort_unless($activityTypeCheck, 404);\n\n $notificationId = $request->input('nId');\n if ($notificationId) {\n /** @var DatabaseNotification|null $notification */\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $view = $request->input('view', 'page');\n\n $activity->loadMissing([\n 'questions.participant',\n 'participants.activity',\n 'topicTriggers',\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n ]);\n\n $data = Fractal::create()\n ->item(\n $activity,\n $transformer->setConsumer($user)\n )\n ->serializeWith(new JsonSerializer())\n ->toArray();\n\n $data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);\n\n if (! isset($data['playbackData']['tracks'])) {\n $data['playbackData']['tracks'] = [];\n }\n\n /**\n * Sending 'playbackVisited' event to Planhat without slowing the\n * response to the user e.g. after the response is sent back.\n */\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'playbackVisited',\n payload: [\n 'activityId' => $activity->getId(),\n 'activityUuid' => $activity->getUuid(),\n ]\n )\n )->always();\n\n return $this->render([\n 'playbackData' => [\n 'activity' => $data['playbackData'],\n 'favorited' => $data['favorited'],\n 'subscribed' => $data['subscribed'],\n 'view' => $view,\n ],\n ]);\n }\n\n private function getPreloadedPlaylist(Activity $activity): array\n {\n $masterPlaylist = [];\n $urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;\n\n $this->authorize('stream', $activity);\n\n $masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);\n $masterPlaylist['placeholder'] = $urlPlaceholder;\n $masterPlaylist['tracks'] = [];\n\n /** @var Models\\Track $track */\n foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {\n $mediaPlaylistPath = $this->mediaPlaylistPath($track);\n $masterPlaylist['tracks'][] = [\n 'id' => $track->getUuid(),\n 'path' => $mediaPlaylistPath,\n ];\n }\n\n return $masterPlaylist;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function playlist(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);\n\n return response($masterPlaylist)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n /**\n * Generate a VTT \"Video Text Tracks\" file.\n *\n * @throws AuthorizationException\n */\n public function vtt(Activity $activity): Response\n {\n $this->authorize('stream', $activity);\n\n $vtt = $this->playbackService->generateVtt($activity);\n\n return response($vtt)\n ->header('Content-Type', 'text/vtt;charset=utf-8');\n }\n\n /**\n * @throws AuthorizationException\n */\n public function media(Track $track): Response\n {\n $this->authorize('stream', $track->activity);\n\n $this->queueMediaCookies($track);\n\n $payload = $this->playbackService->generateMediaPlaylist($track);\n\n return response($payload)\n ->header('Content-Type', 'application/x-mpegURL');\n }\n\n private function mediaPlaylistPath(Track $track): string\n {\n $this->queueMediaCookies($track);\n\n // @TODO return cdn when CORS is fixed\n // return client_cdn($track->content_path, $track->activity->user->team);\n return route('media', ['track' => $track->id_string]);\n }\n\n private function queueMediaCookies(Track $track): void\n {\n $keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;\n if (Cookie::has($keepAliveCookieName)) {\n return;\n }\n\n // Restrict segment URLs to the IP requesting it.\n $remoteIp = request()->ip();\n $cookies = $this->playbackService->generateCookies($track, $remoteIp);\n\n $keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;\n\n // Cookie is only valid for this particular stream path.\n $trackPath = '/' . preg_replace('/\\/[^\\/]+$/', '/', $track->content_path);\n $host = config('jiminny.client_cdn_signed_cookie_domain');\n\n // Queue up cookies to be able to be served secure track media.\n foreach ($cookies as $name => $cookie) {\n Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);\n }\n\n // Cookie is only valid for this particular activity.\n $paths = [\n route('activity.playback', $track->activity->id_string, false),\n route('media', ['track' => $track->id_string], false),\n ];\n foreach ($paths as $path) {\n Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);\n }\n }\n\n /**\n * Used by the Web app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function download(Activity $activity): RedirectResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Download failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return redirect($url);\n }\n\n /**\n * Used by the Mobile app to download the activity.\n *\n * @throws AuthorizationException\n */\n public function getDownloadUrl(Activity $activity): JsonResponse\n {\n $this->authorize('download', $activity);\n\n try {\n $url = $this->downloadActivityService->generateDownloadUrl($activity);\n } catch (\\Throwable $e) {\n Log::info(\n __METHOD__ . ' Getting signed url failed.',\n ['activity' => $activity->getUuid(), 'message' => $e->getMessage()]\n );\n abort(404, $e->getMessage());\n }\n\n return new JsonResponse(\n ['activity_url' => $url],\n JsonResponse::HTTP_OK\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42918882,"top":0.09736632,"width":0.57081115,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.42918882,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.42918882,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.42918882,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.42918882,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.42918882,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.42918882,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.42918882,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.42918882,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.42918882,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.42918882,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.42918882,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.42918882,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.42918882,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.42918882,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.42918882,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.42918882,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.42918882,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.42918882,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.42918882,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.42918882,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.42918882,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.42918882,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.42918882,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.42918882,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3876230358461125011
|
-4259980196618770484
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
6
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\PlaybackPage\Download\Services\DownloadActivityService;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\PlaybackPageTransformer;
use Jiminny\Models\User;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Track;
use Jiminny\Services\PlanhatService;
use Jiminny\Services\PlaybackService;
use JsonException;
use Spatie\Fractal\Fractal;
use Illuminate\Support\Facades\Cookie;
final class PlaybackController extends FrontendController
{
use AuthorizesRequests;
public function __construct(
private readonly PlaybackService $playbackService,
private readonly DownloadActivityService $downloadActivityService,
private readonly PlanhatService $planhatService,
) {
}
/**
* @throws AuthorizationException
* @throws JsonException
*/
public function show(Activity $activity, PlaybackPageTransformer $transformer, Request $request): array|string
{
$this->authorize('view', $activity);
/** @var User $user */
$user = $request->user();
$activityTypeCheck = in_array(
$activity->type,
[
Activity::TYPE_CONFERENCE,
Activity::TYPE_SOFTPHONE,
Activity::TYPE_SOFTPHONE_INBOUND,
],
true
);
abort_unless($activityTypeCheck, 404);
$notificationId = $request->input('nId');
if ($notificationId) {
/** @var DatabaseNotification|null $notification */
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$view = $request->input('view', 'page');
$activity->loadMissing([
'questions.participant',
'participants.activity',
'topicTriggers',
'topicTriggers.participant',
'topicTriggers.playbackThemeTopicTrigger',
'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',
]);
$data = Fractal::create()
->item(
$activity,
$transformer->setConsumer($user)
)
->serializeWith(new JsonSerializer())
->toArray();
$data['playbackData']['masterPlaylist'] = $this->getPreloadedPlaylist($activity);
if (! isset($data['playbackData']['tracks'])) {
$data['playbackData']['tracks'] = [];
}
/**
* Sending 'playbackVisited' event to Planhat without slowing the
* response to the user e.g. after the response is sent back.
*/
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'playbackVisited',
payload: [
'activityId' => $activity->getId(),
'activityUuid' => $activity->getUuid(),
]
)
)->always();
return $this->render([
'playbackData' => [
'activity' => $data['playbackData'],
'favorited' => $data['favorited'],
'subscribed' => $data['subscribed'],
'view' => $view,
],
]);
}
private function getPreloadedPlaylist(Activity $activity): array
{
$masterPlaylist = [];
$urlPlaceholder = PlaybackService::M3U8_TRACK_PLACEHOLDER;
$this->authorize('stream', $activity);
$masterPlaylist['m3u8'] = $this->playbackService->generateMasterPlaylist($activity, null, $urlPlaceholder);
$masterPlaylist['placeholder'] = $urlPlaceholder;
$masterPlaylist['tracks'] = [];
/** @var Models\Track $track */
foreach ($this->playbackService->getMasterPlaylistTracks($activity) as $track) {
$mediaPlaylistPath = $this->mediaPlaylistPath($track);
$masterPlaylist['tracks'][] = [
'id' => $track->getUuid(),
'path' => $mediaPlaylistPath,
];
}
return $masterPlaylist;
}
/**
* @throws AuthorizationException
*/
public function playlist(Activity $activity): Response
{
$this->authorize('stream', $activity);
$masterPlaylist = $this->playbackService->generateMasterPlaylist($activity);
return response($masterPlaylist)
->header('Content-Type', 'application/x-mpegURL');
}
/**
* Generate a VTT "Video Text Tracks" file.
*
* @throws AuthorizationException
*/
public function vtt(Activity $activity): Response
{
$this->authorize('stream', $activity);
$vtt = $this->playbackService->generateVtt($activity);
return response($vtt)
->header('Content-Type', 'text/vtt;charset=utf-8');
}
/**
* @throws AuthorizationException
*/
public function media(Track $track): Response
{
$this->authorize('stream', $track->activity);
$this->queueMediaCookies($track);
$payload = $this->playbackService->generateMediaPlaylist($track);
return response($payload)
->header('Content-Type', 'application/x-mpegURL');
}
private function mediaPlaylistPath(Track $track): string
{
$this->queueMediaCookies($track);
// @TODO return cdn when CORS is fixed
// return client_cdn($track->content_path, $track->activity->user->team);
return route('media', ['track' => $track->id_string]);
}
private function queueMediaCookies(Track $track): void
{
$keepAliveCookieName = 'Media-KeepAlive_' . $track->id_string;
if (Cookie::has($keepAliveCookieName)) {
return;
}
// Restrict segment URLs to the IP requesting it.
$remoteIp = request()->ip();
$cookies = $this->playbackService->generateCookies($track, $remoteIp);
$keepAliveDuration = PlaybackService::MEDIA_COOKIE_MINIMUM_DURATION / 60;
// Cookie is only valid for this particular stream path.
$trackPath = '/' . preg_replace('/\/[^\/]+$/', '/', $track->content_path);
$host = config('jiminny.client_cdn_signed_cookie_domain');
// Queue up cookies to be able to be served secure track media.
foreach ($cookies as $name => $cookie) {
Cookie::queue($name, $cookie, $keepAliveDuration, $trackPath, $host, true, true);
}
// Cookie is only valid for this particular activity.
$paths = [
route('activity.playback', $track->activity->id_string, false),
route('media', ['track' => $track->id_string], false),
];
foreach ($paths as $path) {
Cookie::queue($keepAliveCookieName, 1, $keepAliveDuration, $path, $host, true, true);
}
}
/**
* Used by the Web app to download the activity.
*
* @throws AuthorizationException
*/
public function download(Activity $activity): RedirectResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Download failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return redirect($url);
}
/**
* Used by the Mobile app to download the activity.
*
* @throws AuthorizationException
*/
public function getDownloadUrl(Activity $activity): JsonResponse
{
$this->authorize('download', $activity);
try {
$url = $this->downloadActivityService->generateDownloadUrl($activity);
} catch (\Throwable $e) {
Log::info(
__METHOD__ . ' Getting signed url failed.',
['activity' => $activity->getUuid(), 'message' => $e->getMessage()]
);
abort(404, $e->getMessage());
}
return new JsonResponse(
['activity_url' => $url],
JsonResponse::HTTP_OK
);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16797
|
NULL
|
NULL
|
NULL
|